Vantagens da programação sem estado?


131

Recentemente, aprendi sobre programação funcional (especificamente Haskell, mas também passei por tutoriais sobre Lisp e Erlang). Embora tenha achado os conceitos muito esclarecedores, ainda não vejo o lado prático do conceito "sem efeitos colaterais". Quais são as vantagens práticas disso? Estou tentando pensar na mentalidade funcional, mas há algumas situações que parecem muito complexas sem a capacidade de salvar o estado de uma maneira fácil (não considero as mônadas de Haskell 'fáceis').

Vale a pena continuar aprendendo Haskell (ou outra linguagem puramente funcional) em profundidade? A programação funcional ou sem estado é realmente mais produtiva do que processual? É provável que continuarei a usar o Haskell ou outra linguagem funcional posteriormente, ou devo aprender apenas para o entendimento?

Preocupo-me menos com desempenho do que com produtividade. Então, estou perguntando principalmente se serei mais produtivo em uma linguagem funcional do que em procedimentos / orientados a objetos / o que for.

Respostas:


167

Leia Programação Funcional em poucas palavras .

Existem muitas vantagens na programação sem estado, sendo que o código é dramaticamente multithread e simultâneo. Para ser franco, o estado mutável é inimigo do código multithread. Se os valores são imutáveis ​​por padrão, os programadores não precisam se preocupar com um thread alterando o valor do estado compartilhado entre dois threads, eliminando toda uma classe de bugs de multithreading relacionados às condições de corrida. Como não há condições de corrida, também não há razão para usar bloqueios, então a imutabilidade elimina outra classe inteira de bugs relacionados a deadlocks.

Essa é a grande razão pela qual a programação funcional é importante, e provavelmente a melhor para pular no trem de programação funcional. Também existem muitos outros benefícios, incluindo depuração simplificada (ou seja, funções são puras e não mudam de estado em outras partes de um aplicativo), código mais conciso e expressivo, menos código padrão em comparação com idiomas que são fortemente dependentes de padrões de design e o compilador pode otimizar mais agressivamente seu código.


5
Eu segundo isso! Acredito que a programação funcional será usada muito mais amplamente no futuro devido à sua adequação à programação paralela.
Ray Hidayat

@ Ray: Eu também adicionaria programação distribuída!
Anton Tykhyy 10/05/09

A maior parte disso é verdade, exceto a depuração. Geralmente é mais difícil no haskell, porque você não tem uma pilha de chamadas real, apenas uma pilha de correspondência de padrões. E é muito mais difícil prever como o seu código acaba.
hasufell

3
Além disso, a programação funcional não é realmente sobre "sem estado". A recursão já é um estado implícito (local) e é a principal coisa que fazemos no haskell. Isso fica claro quando você implementa alguns algoritmos não triviais no haskell idiomático (por exemplo, material de geometria computacional) e se diverte depurando-os.
hasufell

2
Não gosto de equiparar os apátridas ao FP. Muitos programas de FP são preenchidos com o estado, ele simplesmente existe em um fechamento e não em um objeto.
mikemaccana

46

Quanto mais partes do seu programa não tiverem estado, mais maneiras existem de juntar as peças sem quebrar nada . O poder do paradigma apátrida não reside na apatridia (ou pureza) per se , mas na capacidade que ele lhe dá de escrever funções poderosas e reutilizáveis e combiná-las.

Você pode encontrar um bom tutorial com muitos exemplos no artigo de John Hughes, Por que a Programação Funcional é Importante (PDF).

Você será gobs mais produtivo, especialmente se você escolher uma linguagem funcional, que também tem tipos de dados algébricos e correspondência de padrão (Caml, SML, Haskell).


Os mixins também não forneceriam código reutilizável de maneira semelhante ao OOP? Não advogando OOP, apenas tentando entender as coisas.
mikemaccana

20

Muitas das outras respostas se concentraram no lado do desempenho (paralelismo) da programação funcional, que acredito ser muito importante. No entanto, você perguntou especificamente sobre produtividade, como em: é possível programar a mesma coisa mais rapidamente em um paradigma funcional do que em um paradigma imperativo.

Na verdade, acho (por experiência pessoal) que a programação em F # corresponde à maneira como penso melhor e, portanto, é mais fácil. Eu acho que é a maior diferença. Eu programei em F # e C #, e há muito menos "lutando contra a linguagem" em F #, que eu amo. Você não precisa pensar nos detalhes em F #. Aqui estão alguns exemplos do que eu realmente gostei.

Por exemplo, mesmo que o F # seja digitado estaticamente (todos os tipos são resolvidos em tempo de compilação), a inferência de tipo descobre quais tipos você possui, para que você não precise dizer isso. E se não conseguir descobrir, ele automaticamente torna sua função / classe / qualquer que seja genérico. Portanto, você nunca precisa escrever nenhum genérico, tudo é automático. Acho que isso significa que estou gastando mais tempo pensando no problema e menos em como implementá-lo. De fato, sempre que volto ao C #, sinto muita falta dessa inferência de tipo, você nunca percebe o quão perturbador é até que não precise mais fazer isso.

Também em F #, em vez de escrever loops, você chama funções. É uma mudança sutil, mas significativa, porque você não precisa mais pensar na construção do loop. Por exemplo, aqui está um código que corresponderia a algo (não me lembro o quê, é de um quebra-cabeça de Euler do projeto):

let matchingFactors =
    factors
    |> Seq.filter (fun x -> largestPalindrome % x = 0)
    |> Seq.map (fun x -> (x, largestPalindrome / x))

Sei que fazer um filtro e depois um mapa (que é uma conversão de cada elemento) em C # seria bastante simples, mas é preciso pensar em um nível inferior. Particularmente, você teria que escrever o loop em si e ter sua própria declaração if explícita e esse tipo de coisa. Desde que aprendi F #, percebi que achei mais fácil codificar da maneira funcional. Onde, se você deseja filtrar, escreve "filter" e, se deseja mapear, escreve "map", em vez de implementar cada um dos detalhes.

Também adoro o operador |>, que acho que separa o F # do ocaml e possivelmente outras linguagens funcionais. É o operador de pipe, permite "canalizar" a saída de uma expressão na entrada de outra expressão. Faz o código seguir como penso mais. Como no snippet de código acima, está dizendo: "pegue a sequência de fatores, filtre-a e mapeie-a". É um nível de pensamento muito alto, que você não consegue em uma linguagem de programação imperativa porque está muito ocupado escrevendo o loop e as declarações if. É a única coisa que mais sinto falta sempre que vou para outro idioma.

Portanto, em geral, mesmo que eu possa programar em C # e F #, acho mais fácil usar o F # porque você pode pensar em um nível superior. Eu diria que, como os detalhes menores são removidos da programação funcional (pelo menos em F #), sou mais produtivo.

Edit : Eu vi em um dos comentários que você pediu um exemplo de "estado" em uma linguagem de programação funcional. F # pode ser escrito imperativamente, então aqui está um exemplo direto de como você pode ter um estado mutável em F #:

let mutable x = 5
for i in 1..10 do
    x <- x + i

1
Eu concordo com o seu post em geral, mas |> nada tem a ver com programação funcional em si. Na verdade, a |> b (p1, p2)é apenas açúcar sintático para b (a, p1, p2). Junte isso à associatividade correta e você terá.
Anton Tykhyy

2
É verdade que devo reconhecer que provavelmente muito da minha experiência positiva com o F # tem mais a ver com o F # do que com a programação funcional. Mas, ainda assim, existe uma forte correlação entre os dois, e mesmo que coisas como inferência de tipo e |> não sejam programação funcional em si, certamente eu diria que eles "vão com o território". Pelo menos em geral.
Ray Hidayat

|> é apenas outra função infix de ordem superior, neste caso, um operador de aplicativo de função. Definir seus próprios operadores infix de ordem superior é definitivamente uma parte da programação funcional (a menos que você seja um Schemer). Haskell tem seu $, que é o mesmo, exceto que as informações no pipeline fluem da direita para a esquerda.
Norman Ramsey

15

Considere todos os erros difíceis que você passou muito tempo depurando.

Agora, quantos desses erros foram causados ​​por "interações não desejadas" entre dois componentes separados de um programa? (Quase todos os bugs de encadeamento têm este formato: corridas envolvendo a gravação de dados compartilhados, deadlocks, ... Além disso, é comum encontrar bibliotecas que tenham algum efeito inesperado no estado global, ou ler / gravar o registro / ambiente, etc.) I afirmaria que pelo menos 1 em cada 3 'hard bugs' se enquadram nessa categoria.

Agora, se você mudar para a programação sem estado / imutável / pura, todos esses erros desaparecem. Você é apresentado com alguns novos desafios em vez (por exemplo, quando você faz querer diferentes módulos para interagir com o ambiente), mas em uma linguagem como Haskell, essas interações se explicitamente reificada no sistema tipo, o que significa que você pode apenas olhar para o tipo de uma função e uma razão sobre o tipo de interações que ele pode ter com o restante do programa.

Essa é a grande vitória da IMO de 'imutabilidade'. Em um mundo ideal, todos nós projetamos APIs fantásticas e, mesmo quando as coisas eram mutáveis, os efeitos seriam locais e bem documentados e as interações 'inesperadas' seriam reduzidas ao mínimo. No mundo real, existem muitas APIs que interagem com o estado global de inúmeras maneiras, e essas são a fonte dos erros mais perniciosos. Aspirar à apatridia é aspirar a se livrar de interações não intencionais / implícitas / nos bastidores entre os componentes.


6
Alguém disse uma vez que sobrescrever um valor mutável significa que você está explicitamente coletando / liberando o valor anterior. Em alguns casos, outras partes do programa não foram concluídas usando esse valor. Quando os valores não podem ser alterados, essa classe de erros também desaparece.
2111 shapr

8

Uma vantagem das funções sem estado é que elas permitem o pré-cálculo ou o armazenamento em cache dos valores de retorno da função. Mesmo alguns compiladores C permitem marcar explicitamente as funções como sem estado para melhorar sua otimização. Como muitos outros observaram, funções sem estado são muito mais fáceis de paralelizar.

Mas a eficiência não é a única preocupação. Uma função pura é mais fácil de testar e depurar, pois qualquer coisa que a afeta é explicitamente declarada. E, ao programar em uma linguagem funcional, o hábito de tornar o mínimo possível de funções "sujas" (com E / S, etc.). Separar as coisas com estado dessa maneira é uma boa maneira de criar programas, mesmo em linguagens não tão funcionais.

Os idiomas funcionais podem demorar um pouco para "chegar", e é difícil explicar para alguém que não passou por esse processo. Mas a maioria das pessoas que persiste por tempo suficiente finalmente percebe que a confusão vale a pena, mesmo que não acabem usando muito as linguagens funcionais.


Essa primeira parte é um ponto realmente interessante, eu nunca tinha pensado nisso antes. Obrigado!
Sasha Chedygov 10/05/09

Suponha que você tenha sin(PI/3)em seu código, onde PI é uma constante, o compilador possa avaliar essa função em tempo de compilação e incorporar o resultado no código gerado.
Artelius #

6

Sem estado, é muito fácil paralelizar automaticamente seu código (como as CPUs são feitas com mais e mais núcleos, isso é muito importante).


Sim, eu definitivamente olhei para isso. O modelo de concorrência de Erlang, em particular, é muito intrigante. No entanto, neste momento, eu realmente não me importo tanto com simultaneidade quanto com produtividade. Existe um bônus de produtividade na programação sem estado?
Sasha Chedygov 10/10/2009

2
@musicfreak, não, não há um bônus de produtividade. Mas como nota, as linguagens FP modernas ainda permitem que você use o estado, se você realmente precisa.
Desconhecido

Realmente? Você pode dar um exemplo de estado em uma linguagem funcional, apenas para que eu possa ver como isso é feito?
Sasha Chedygov

Confira a State Monad
rampion

4
@Desconhecido: eu discordo. A programação sem estado reduz a ocorrência de erros devido a interações imprevistas / não intencionais de diferentes componentes. Também incentiva um melhor design (mais reutilização, separação de mecanismos e políticas e esse tipo de coisa). Nem sempre é apropriado para a tarefa em questão, mas em alguns casos realmente brilha.
Artelius

6

Aplicativos da web sem estado são essenciais quando você começa a ter um tráfego maior.

Pode haver muitos dados do usuário que você não deseja armazenar no lado do cliente por motivos de segurança, por exemplo. Nesse caso, você precisa armazená-lo no lado do servidor. Você pode usar a sessão padrão dos aplicativos da web, mas se tiver mais de uma instância do aplicativo, precisará garantir que cada usuário esteja sempre direcionado para a mesma instância.

Os balanceadores de carga geralmente têm a capacidade de ter 'sessões fixas', onde o balanceador de carga sabe como para qual servidor enviar os usuários. Porém, isso não é ideal; por exemplo, significa que toda vez que você reiniciar o aplicativo da Web, todos os usuários conectados perderão a sessão.

Uma abordagem melhor é armazenar a sessão atrás dos servidores Web em algum tipo de armazenamento de dados; atualmente, existem muitos produtos nosql disponíveis para isso (redis, mongo, elasticsearch, memcached). Dessa forma, os servidores web não têm estado, mas você ainda possui o estado do servidor e a disponibilidade desse estado pode ser gerenciada escolhendo a configuração correta do armazenamento de dados. Esses armazenamentos de dados geralmente têm uma grande redundância, portanto, quase sempre deve ser possível fazer alterações em seu aplicativo Web e até mesmo no armazenamento de dados sem afetar os usuários.


Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.