Como funcionam as linguagens de programação funcional?

92

Se as linguagens de programação funcional não podem salvar nenhum estado, como eles fazem coisas simples como ler a entrada de um usuário? Como eles "armazenam" a entrada (ou armazenam quaisquer dados para esse assunto?)

Por exemplo: como essa coisa simples em C se traduziria em uma linguagem de programação funcional como Haskell?

#include<stdio.h>
int main() {
    int no;
    scanf("%d",&no);
    return 0;
}

(Minha pergunta foi inspirada por este excelente post: "Execução no Reino dos Substantivos" . A leitura me deu um melhor entendimento do que exatamente é a programação orientada a objetos, como Java a implementa de uma maneira extrema e como as linguagens de programação funcionais são uma contraste.)

lazer
fonte
possível duplicata de stackoverflow.com/questions/1916692/…
Chris Conway
4
É uma boa pergunta, porque em um nível sistêmico um computador precisa de um estado para ser útil. Assisti a uma entrevista com Simon Peyton-Jones (um dos desenvolvedores por trás de Haskell), onde ele disse que um computador que só rodava software totalmente sem estado só poderia realizar uma coisa: esquentar! Muitas boas respostas abaixo. Existem duas estratégias principais: 1) Faça uma linguagem impura. 2) Faça um plano astuto para abstrair o estado, que é o que Haskell faz, essencialmente criando um novo mundo ligeiramente modificado em vez de modificar o antigo.
prejudica
14
O SPJ não estava falando sobre os efeitos colaterais, não sobre o estado? Os cálculos puros têm muitos estados implícitos nas ligações de argumento e na pilha de chamadas, mas sem efeitos colaterais (por exemplo, E / S) não podem fazer nada útil. Os dois pontos são realmente bem distintos - há toneladas de código Haskell puro e dinâmico, e a Statemônada é muito elegante; por outro lado, IOé um hack feio e sujo, usado apenas com relutância.
CA McCann
4
camccann está certo. Há muito estado nas linguagens funcionais. É apenas gerenciado explicitamente em vez de "ação fantasmagórica à distância" como nas linguagens imperativas.
APENAS MINHA OPINIÃO correta
1
Pode haver alguma confusão aqui. Talvez os computadores precisem de efeitos para serem úteis, mas acho que a questão aqui é sobre linguagens de programação, não computadores.
Conal

Respostas:

80

Se as linguagens de programação funcional não podem salvar nenhum estado, como eles fazem algumas coisas simples, como ler a entrada de um usuário (quero dizer, como eles "armazenam"), ou armazenar quaisquer dados para esse assunto?

Como você percebeu, a programação funcional não tem estado - mas isso não significa que não pode armazenar dados. A diferença é que se eu escrever uma declaração (Haskell) ao longo das linhas de

let x = func value 3.14 20 "random"
in ...

Tenho a garantia de que o valor de xé sempre o mesmo em ...: nada pode alterá-lo. Da mesma forma, se eu tiver uma função f :: String -> Integer(uma função que recebe uma string e retorna um inteiro), posso ter certeza de que fnão modificarei seu argumento, nem alterarei nenhuma variável global, nem gravarei dados em um arquivo e assim por diante. Como sepp2k disse em um comentário acima, esta não mutabilidade é realmente útil para raciocinar sobre programas: você escreve funções que dobram, giram e mutilam seus dados, retornando novas cópias para que você possa encadea-los, e você pode ter certeza de que nenhum dessas chamadas de função pode fazer qualquer coisa "prejudicial". Você sabe que xé sempre x, e não precisa se preocupar se alguém escreveu x := foo barem algum lugar entre a declaração dex e seu uso, porque isso é impossível.

Agora, e se eu quiser ler a entrada de um usuário? Como KennyTM disse, a ideia é que uma função impura é uma função pura que é passada para o mundo inteiro como um argumento e retorna seu resultado e o mundo. Claro, você não quer realmente fazer isso: por um lado, é terrivelmente desajeitado e, por outro, o que acontece se eu reutilizar o mesmo objeto de mundo? Portanto, isso é abstraído de alguma forma. Haskell lida com isso com o tipo IO:

main :: IO ()
main = do str <- getLine
          let no = fst . head $ reads str :: Integer
          ...

Isso nos diz que mainé uma ação IO que não retorna nada; executar esta ação é o que significa executar um programa Haskell. A regra é que os tipos IO nunca podem escapar de uma ação IO; neste contexto, apresentamos essa ação usando do. Assim, getLineretorna um IO String, que pode ser pensado de duas maneiras: primeiro, como uma ação que, quando executada, produz uma string; em segundo lugar, como uma string "manchada" por IO, pois foi obtida de maneira impura. O primeiro é mais correto, mas o segundo pode ser mais útil. O <-tira o Stringdo IO Stringe o armazena - strmas como estamos em uma ação de IO, teremos que embrulhá-lo novamente, para que não possa "escapar". A próxima linha tenta ler um inteiro ( reads) e pega a primeira correspondência bem-sucedida (fst . head); tudo isso é puro (sem IO), então damos um nome a ele com let no = .... Podemos então usar noe strno .... Assim, armazenamos dados impuros (de getLineem str) e dados puros ( let no = ...).

Este mecanismo para trabalhar com IO é muito poderoso: ele permite separar a parte pura e algorítmica de seu programa da parte impura de interação com o usuário, e reforça isso no nível de tipo. Sua minimumSpanningTreefunção não pode mudar algo em algum outro lugar em seu código ou escrever uma mensagem para seu usuário e assim por diante. É seguro.

Isso é tudo que você precisa saber para usar IO em Haskell; se isso é tudo que você quer, pode parar por aqui. Mas se você quiser entender por que isso funciona, continue lendo. (E observe que essas coisas serão específicas para Haskell - outras linguagens podem escolher uma implementação diferente.)

Portanto, isso provavelmente parecia uma trapaça, de alguma forma adicionando impureza ao puro Haskell. Mas não é - acontece que podemos implementar o tipo IO inteiramente dentro de Haskell puro (contanto que tenhamos RealWorld). A ideia é esta: uma ação IO IO typeé o mesmo que uma função RealWorld -> (type, RealWorld), que pega o mundo real e retorna um objeto do tipo typee o modificado RealWorld. Em seguida, definimos algumas funções para que possamos usar este tipo sem enlouquecer:

return :: a -> IO a
return a = \rw -> (a,rw)

(>>=) :: IO a -> (a -> IO b) -> IO b
ioa >>= fn = \rw -> let (a,rw') = ioa rw in fn a rw'

O primeiro nos permite falar sobre ações IO que não fazem nada: return 3é uma ação IO que não consulta o mundo real e apenas retorna 3. O >>=operador, pronunciado "vincular", nos permite executar ações IO. Ele extrai o valor da ação IO, passa-o e ao mundo real por meio da função e retorna a ação IO resultante. Observe que isso >>=impõe nossa regra de que os resultados das ações de IO nunca podem escapar.

Podemos então transformar o anterior mainno seguinte conjunto comum de aplicativos de função:

main = getLine >>= \str -> let no = (fst . head $ reads str :: Integer) in ...

O tempo de execução de Haskell começa maincom o inicial RealWorlde está pronto! Tudo é puro, tem apenas uma sintaxe sofisticada.

[ Editar: como @Conal aponta , isso não é realmente o que Haskell usa para fazer IO. Este modelo quebra se você adicionar simultaneidade, ou mesmo qualquer forma de o mundo mudar no meio de uma ação de IO, portanto, seria impossível para Haskell usar este modelo. É preciso apenas para computação sequencial. Portanto, pode ser que o IO de Haskell seja um pouco um esquivo; mesmo se não for, certamente não é tão elegante. De acordo com a observação de @ Conal, veja o que Simon Peyton-Jones diz em Tackling the Awkward Squad [pdf] , seção 3.1; ele apresenta o que pode equivaler a um modelo alternativo ao longo dessas linhas, mas depois o descarta por sua complexidade e adota uma abordagem diferente.]

Novamente, isso explica (praticamente) como o IO e a mutabilidade em geral funcionam em Haskell; se isso é tudo que você quer saber, pode parar de ler aqui. Se você quiser uma última dose de teoria, continue lendo - mas lembre-se, neste ponto, já fomos muito longe de sua pergunta!

Então, uma última coisa: acontece que essa estrutura - um tipo paramétrico com returne >>=- é muito geral; é chamado de mônada, e a donotação return, e >>=funciona com qualquer um deles. Como você viu aqui, as mônadas não são mágicas; tudo o que é mágico é que os doblocos se transformam em chamadas de função. O RealWorldtipo é o único lugar onde vemos alguma magia. Tipos como [], o construtor de lista, também são mônadas e não têm nada a ver com código impuro.

Você agora sabe (quase) tudo sobre o conceito de mônada (exceto algumas leis que devem ser satisfeitas e a definição matemática formal), mas falta-lhe intuição. Há um número ridículo de tutoriais de mônadas online; Eu gosto deste , mas você tem opções. No entanto, isso provavelmente não o ajudará ; a única maneira real de obter a intuição é combinando usá-los com a leitura de alguns tutoriais no momento certo.

No entanto, você não precisa dessa intuição para entender IO . Compreender as mônadas em geral é a cereja do bolo, mas você pode usar IO agora. Você poderia usá-lo depois que eu mostrasse a primeira mainfunção. Você pode até tratar o código IO como se estivesse em uma linguagem impura! Mas lembre-se de que há uma representação funcional subjacente: ninguém está trapaceando.

(PS: Desculpe pela extensão. Fui um pouco mais longe.)

Antal Spector-Zabusky
fonte
6
O que sempre me impressiona em Haskell (que eu fiz e estou me esforçando muito para aprender) é a feiura da sintaxe. É como se eles pegassem os piores pedaços de todas as outras línguas, jogassem em um balde e mexessem furiosamente. E essas pessoas reclamarão da sintaxe reconhecidamente estranha (em alguns lugares) do C ++!
19
Neil: Sério? Na verdade, acho a sintaxe de Haskell muito limpa. Estou curioso; a que em particular você está se referindo? (Pelo que vale a pena, C ++ também não me incomoda, exceto pela necessidade de fazer > >nos modelos.)
Antal Spector-Zabusky
6
A meu ver, embora a sintaxe de Haskell não seja tão limpa como, digamos, Scheme, ela não começa a se comparar à sintaxe hedionda de, bem, até mesmo a mais agradável das linguagens de chaves, das quais C ++ está entre as piores . Sem consideração pelo gosto, suponho. Não acho que exista uma linguagem que todos achem sintaticamente agradável.
CA McCann
8
@NeilButterworth: Suspeito que seu problema não seja tanto a sintaxe, mas os nomes das funções. Se funções como >>=ou $tivessem mais onde, em vez disso , fossem chamadas binde apply, o código haskell se pareceria muito menos com perl. Quero dizer, a principal diferença entre haskell e sintaxe de esquema é que haskell tem operadores infixados e parênteses opcionais. Se as pessoas evitassem o uso excessivo de operadores de infixo, o haskell se pareceria muito com o esquema com menos parênteses.
sepp2k de
5
@camcann: Bem, aponte, mas o que eu quis dizer é: A sintaxe básica do esquema é (functionName arg1 arg2). Se você remover os parênteses, functionName arg1 arg2é a sintaxe haskell. Se você permitir operadores infixados com nomes arbitrariamente horríveis, você obterá o arg1 §$%&/*°^? arg2que é ainda mais parecido com haskell. (Estou apenas brincando, na verdade eu gosto de haskell).
sepp2k de
23

Muitas respostas boas aqui, mas são longas. Vou tentar dar uma resposta curta útil:

  • Linguagens funcionais colocam estado nos mesmos lugares que C: em variáveis ​​nomeadas e em objetos alocados no heap. As diferenças são que:

    • Em uma linguagem funcional, uma "variável" obtém seu valor inicial quando entra no escopo (por meio de uma chamada de função ou ligação let), e esse valor não muda depois . Da mesma forma, um objeto alocado no heap é inicializado imediatamente com os valores de todos os seus campos, que não mudam depois disso.

    • "Mudanças de estado" tratadas não pela mutação de variáveis ​​ou objetos existentes, mas pela vinculação de novas variáveis ​​ou alocação de novos objetos.

  • O IO funciona por meio de um truque. Um cálculo de efeito colateral que produz uma string é descrito por uma função que recebe um World como argumento e retorna um par contendo a string e um novo World. O mundo inclui o conteúdo de todas as unidades de disco, o histórico de cada pacote de rede já enviado ou recebido, a cor de cada pixel na tela e coisas assim. A chave para o truque é que o acesso ao mundo é cuidadosamente restrito para que

    • Nenhum programa pode fazer uma cópia do World (onde você colocaria?)

    • Nenhum programa pode jogar fora o mundo

    O uso desse truque possibilita que haja um único mundo cujo estado evolui com o tempo. O sistema de tempo de execução da linguagem, que não é escrito em uma linguagem funcional, implementa uma computação de efeito colateral atualizando o mundo único no local em vez de retornar um novo.

    Este truque é lindamente explicado por Simon Peyton Jones e Phil Wadler em seu artigo de referência "Programação Funcional Imperativa" .

Norman Ramsey
fonte
4
Pelo que posso dizer, essa IOhistória ( World -> (a,World)) é um mito quando aplicada a Haskell, já que esse modelo explica apenas a computação puramente sequencial, enquanto o IOtipo de Haskell inclui simultaneidade. Por "puramente sequencial", quero dizer que nem mesmo o mundo (universo) pode mudar entre o início e o fim de um cálculo imperativo, exceto devido a esse cálculo. Por exemplo, enquanto seu computador está funcionando, seu cérebro, etc., não consegue. A simultaneidade pode ser tratada por algo mais parecido World -> PowerSet [(a,World)], o que permite não-determinismo e intercalação.
Conal
1
@Conal: Eu acho que a história de IO generaliza muito bem para o não-determinismo e intercalação; se bem me lembro, há uma explicação muito boa no jornal "Awkward Squad". Mas não conheço um bom artigo que explique claramente o verdadeiro paralelismo.
Norman Ramsey
3
Pelo que entendi, o artigo "Awkward Squad" abandona a tentativa de generalizar o modelo denotacional simples de IO, isto é World -> (a,World)(o "mito" popular e persistente a que me referi) e, em vez disso, dá uma explicação operacional. Algumas pessoas gostam de semântica operacional, mas elas me deixam completamente insatisfeito. Por favor, veja minha resposta mais longa em outra resposta.
Conal
+1 Isso me ajudou a entender muito mais as Mônadas de IO, além de responder à pergunta.
CaptainCasey
A maioria dos compiladores Haskell realmente define IOcomo RealWorld -> (a,RealWorld), mas em vez de realmente representar o mundo real, é apenas um valor abstrato que precisa ser transmitido e acaba sendo otimizado pelo compilador.
Jeremy List
19

Estou interrompendo uma resposta de comentário para uma nova resposta, para dar mais espaço:

Eu escrevi:

Pelo que posso dizer, essa IOhistória ( World -> (a,World)) é um mito quando aplicada a Haskell, já que esse modelo explica apenas a computação puramente sequencial, enquanto o IOtipo de Haskell inclui simultaneidade. Por "puramente sequencial", quero dizer que nem mesmo o mundo (universo) pode mudar entre o início e o fim de um cálculo imperativo, exceto devido a esse cálculo. Por exemplo, enquanto seu computador está funcionando, seu cérebro, etc., não consegue. A simultaneidade pode ser tratada por algo mais parecido World -> PowerSet [(a,World)], o que permite não-determinismo e intercalação.

Norman escreveu:

@Conal: Eu acho que a história de IO generaliza muito bem para o não-determinismo e intercalação; se bem me lembro, há uma explicação muito boa no jornal "Awkward Squad". Mas não conheço um bom artigo que explique claramente o verdadeiro paralelismo.

@Norman: Generaliza em que sentido? Estou sugerindo que o modelo / explicação denotacional normalmente dado, World -> (a,World)não corresponde a Haskell IOporque não leva em conta o não determinismo e a simultaneidade. Pode haver um modelo mais complexo que se ajusta, como World -> PowerSet [(a,World)], mas não sei se esse modelo foi elaborado e se mostrou adequado e consistente. Eu pessoalmente duvido que tal besta possa ser encontrada, visto que IOé preenchida por milhares de chamadas de API imperativas importadas por FFI. E, como tal, IOestá cumprindo seu propósito:

Problema aberto: a IOmônada se tornou o pecado de Haskell. (Sempre que não entendemos algo, jogamos na mônada IO.)

(Do discurso POPL de Simon PJ Vestindo a camisa de cabelo Vestindo a camisa de cabelo: uma retrospectiva de Haskell .)

Na Seção 3.1 de Tackling the Awkward Squad , Simon aponta o que não funciona type IO a = World -> (a, World), incluindo "A abordagem não escala bem quando adicionamos concorrência". Ele então sugere um possível modelo alternativo e, em seguida, abandona a tentativa de explicações denotacionais, dizendo

No entanto, em vez disso, vamos adotar uma semântica operacional, baseada em abordagens padrão para a semântica de cálculos de processo.

Essa falha em encontrar um modelo denotacional preciso e útil está na raiz do motivo pelo qual vejo Haskell IO como um afastamento do espírito e os benefícios profundos do que chamamos de "programação funcional", ou o que Peter Landin mais especificamente chamou de "programação denotativa" . Veja os comentários aqui.

Conal
fonte
Obrigado pela resposta mais longa. Eu acho que talvez eu tenha sofrido uma lavagem cerebral por nossos novos senhores operacionais. Movimentadores à esquerda e à direita e assim por diante tornaram possível provar alguns teoremas úteis. Você já viu algum modelo denotacional de que goste que explique o não determinismo e a simultaneidade? Eu não tenho.
Norman Ramsey
1
Gosto de como World -> PowerSet [World]captura nitidamente o não-determinismo e a simultaneidade de estilo intercalado. Esta definição de domínio me diz que a programação imperativa concorrente mainstream (incluindo Haskell) é intratável - literalmente exponencialmente mais complexa do que sequencial. O grande dano que vejo no IOmito de Haskell está obscurecendo essa complexidade inerente, desmotivando sua derrubada.
Conal
Embora eu veja por que World -> (a, World)está quebrado, não estou certo sobre por que a substituição World -> PowerSet [(a,World)]modela corretamente a simultaneidade, etc. Para mim, isso parece implicar que os programas em IOdevem ser executados em algo como a monada de lista, aplicando-se a cada item do conjunto retornado pela IOação. o que estou perdendo?
Antal Spector-Zabusky
3
@Absz: Primeiro, meu modelo sugerido World -> PowerSet [(a,World)]não está certo. Em World -> PowerSet ([World],a)vez disso, vamos tentar . PowerSetfornece o conjunto de resultados possíveis (não determinismo). [World]são sequências de estados intermediários (não a mônada de lista / não determinismo), permitindo a intercalação (escalonamento de thread). E ([World],a)também não está certo, pois permite o acesso aantes de passar por todos os estados intermediários. Em vez disso, defina o uso World -> PowerSet (Computation a)ondedata Computation a = Result a | Step World (Computation a)
Conal
Ainda não vejo problema com World -> (a, World). Se o Worldtipo realmente inclui todo o mundo, então também inclui as informações sobre todos os processos em execução simultaneamente e também a 'semente aleatória' de todo o não-determinismo. O resultado Worldé um mundo com o tempo avançado e alguma interação realizada. O único problema real com este modelo parece ser que ele é muito geral e os valores de Worldnão podem ser construídos e manipulados.
Rotsor
17

A programação funcional deriva do cálculo lambda. Se você realmente deseja entender a programação funcional, confira http://worrydream.com/AlligatorEggs/

É uma maneira "divertida" de aprender cálculo lambda e levá-lo para o emocionante mundo da programação funcional!

Como saber Lambda Calculus é útil na programação funcional.

Então Lambda Calculus é a base para muitas linguagens de programação do mundo real, como Lisp, Scheme, ML, Haskell, ....

Suponha que desejamos descrever uma função que adiciona três a qualquer entrada para fazermos isso, escreveríamos:

plus3 x = succ(succ(succ x)) 

Leia “plus3 é uma função que, quando aplicada a qualquer número x, produz o sucessor do sucessor do sucessor de x”

Observe que a função que adiciona 3 a qualquer número não precisa ser chamada de plus3; o nome “plus3” é apenas uma abreviatura conveniente para nomear esta função

(plus3 x) (succ 0) ≡ ((λ x. (succ (succ (succ x)))) (succ 0))

Observe que usamos o símbolo lambda para uma função (acho que se parece com um jacaré, estou supondo que foi daí que veio a ideia dos ovos de jacaré)

O símbolo lambda é o Jacaré (uma função) e x é sua cor. Você também pode pensar em x como um argumento (na verdade, as funções Lambda Calculus têm apenas um argumento), o resto você pode pensar nele como o corpo da função.

Agora considere a abstração:

g  λ f. (f (f (succ 0)))

O argumento f é usado em uma posição de função (em uma chamada). Chamamos uma função de ordem superior porque ela recebe outra função como entrada. Você pode pensar nas outras chamadas de função f como " ovos ". Agora, pegando as duas funções ou " crocodilos " que criamos, podemos fazer algo assim:

(g plus3) =  f. (f (f (succ 0)))(λ x . (succ (succ (succ x)))) 
= ((λ x. (succ (succ (succ x)))((λ x. (succ (succ (succ x)))) (succ 0)))
 = ((λ x. (succ (succ (succ x)))) (succ (succ (succ (succ 0)))))
 = (succ (succ (succ (succ (succ (succ (succ 0)))))))

Se você notar, pode ver que nosso λ f Jacaré come nosso λ x Jacaré e então o λ x Jacaré e morre. Então, nosso λ x Jacaré renasce nos ovos de Jacaré de λ f. Em seguida, o processo se repete e o λ x Alligator à esquerda agora come o outro λ x Alligator à direita.

Então você pode usar este conjunto simples de regras de " Jacarés " comendo " Jacarés " para projetar uma gramática e, assim, nasceram as linguagens de programação funcional!

Assim, você pode ver se conhece o Lambda Calculus e entenderá como funcionam as linguagens funcionais.

PJT
fonte
@tuckster: Eu estudei cálculo lambda várias vezes antes ... e sim, o artigo da AlligatorEggs faz sentido para mim. Mas não consigo relacionar isso à programação. Para mim, agora, o cálculo labda é como uma teoria separada, que está lá. Como os conceitos de cálculo lambda são usados ​​em linguagens de programação?
Lazer
3
@eSKay: Haskell é cálculo lambda, com uma fina camada de açúcar sintático para torná-lo mais parecido com uma linguagem de programação normal. As linguagens da família Lisp também são muito semelhantes ao cálculo lambda não tipado, que é o que os ovos de jacaré representam. Lambda calculus em si é essencialmente uma linguagem de programação minimalista, como uma espécie de "linguagem assembly de programação funcional".
CA McCann
@eSKay: Eu adicionei um pouco sobre como isso se relaciona com alguns exemplos. Eu espero que isso ajude!
PJT de
Se você vai subtrair da minha resposta, poderia deixar um comentário sobre o motivo para que eu possa tentar melhorar minha resposta. Obrigado.
PJT
14

A técnica para lidar com o estado em Haskell é muito direta. E você não precisa entender as mônadas para entender isso.

Em uma linguagem de programação com estado, você normalmente tem algum valor armazenado em algum lugar, algum código é executado e, em seguida, você tem um novo valor armazenado. Em linguagens imperativas, esse estado está apenas em algum lugar "no fundo". Em uma linguagem funcional (pura), você torna isso explícito, então escreve explicitamente a função que transforma o estado.

Então, em vez de ter algum estado do tipo X, você escreve funções que mapeiam X para X. É isso! Você muda de pensar sobre o estado para pensar sobre quais operações deseja realizar no estado. Você pode então encadear essas funções e combiná-las de várias maneiras para fazer programas inteiros. É claro que você não está limitado a apenas mapear X a X. Você pode escrever funções para obter várias combinações de dados como entrada e retornar várias combinações no final.

As mônadas são uma ferramenta, entre muitas, para ajudar a organizar isso. Mas as mônadas não são realmente a solução para o problema. A solução é pensar em transformações de estado em vez de estado.

Isso também funciona com E / S. Na verdade, o que acontece é o seguinte: em vez de obter a entrada do usuário com algum equivalente direto de scanf, e armazená-la em algum lugar, você escreve uma função para dizer o que faria com o resultado de scanfse o tivesse, e então passa isso função para a API de E / S. Isso é exatamente o que >>=acontece quando você usa a IOmônada em Haskell. Portanto, você nunca precisa armazenar o resultado de qualquer I / O em qualquer lugar - você só precisa escrever um código que diga como gostaria de transformá-lo.

Sigfpe
fonte
8

(Algumas linguagens funcionais permitem funções impuras.)

Para linguagens puramente funcionais , a interação do mundo real geralmente é incluída como um dos argumentos da função, como este:

RealWorld pureScanf(RealWorld world, const char* format, ...);

Linguagens diferentes têm estratégias diferentes para abstrair o mundo do programador. Haskell, por exemplo, usa mônadas para esconder o worldargumento.


Mas a parte pura da linguagem funcional em si já é Turing completa, o que significa que qualquer coisa factível em C também é factível em Haskell. A principal diferença para a linguagem imperativa é, em vez de modificar os estados existentes:

int compute_sum_of_squares (int min, int max) {
  int result = 0;
  for (int i = min; i < max; ++ i)
     result += i * i;  // modify "result" in place
  return result;
}

Você incorpora a parte da modificação em uma chamada de função, geralmente transformando loops em recursões:

int compute_sum_of_squares (int min, int max) {
  if (min >= max)
    return 0;
  else
    return min * min + compute_sum_of_squares(min + 1, max);
}
Kennytm
fonte
Ou apenas computeSumOfSquares min max = sum [x*x | x <- [min..max]];-)
fredoverflow
@Fred: A compreensão da lista é apenas um açúcar sintático (e então você precisa explicar a mônada da lista em detalhes). E como você implementa sum? A recursão ainda é necessária.
kennytm
3

A linguagem funcional pode salvar o estado! Eles geralmente apenas encorajam ou forçam você a ser explícito sobre isso.

Por exemplo, verifique a Mônada Estadual de Haskell .

Shaun
fonte
9
E tenha em mente que não há nada sobre Stateou Monadque habilite o estado, já que ambos são definidos em termos de ferramentas simples, gerais e funcionais. Eles apenas capturam padrões relevantes, então você não precisa reinventar a roda tanto.
Conal
1

haskell:

main = do no <- readLn
          print (no + 1)

Você pode, é claro, atribuir coisas a variáveis ​​em linguagens funcionais. Você simplesmente não pode alterá-los (então basicamente todas as variáveis ​​são constantes em linguagens funcionais).

sepp2k
fonte
@ sepp2k: por que, qual o mal em mudá-los?
Lazer
@eSKay se você não pode mudar as variáveis, então você sabe que elas são sempre iguais. Isso torna mais fácil depurar, força você a criar funções mais simples que fazem apenas uma coisa e muito bem. Também ajuda muito ao trabalhar com simultaneidade.
Henrik Hansen
9
@eSKay: Os programadores funcionais acreditam que o estado mutável introduz muitas possibilidades de bugs e torna mais difícil raciocinar sobre o comportamento dos programas. Por exemplo, se você tem uma chamada de função f(x)e deseja ver qual é o valor de x, basta ir até o local onde x está definido. Se x fosse mutável, você também teria que considerar se há algum ponto onde x poderia ser alterado entre sua definição e seu uso (o que não é trivial se x não for uma variável local).
sepp2k de
6
Não são apenas os programadores funcionais que desconfiam do estado mutável e dos efeitos colaterais. Objetos imutáveis ​​e separação de comando / consulta são bem vistos por alguns programadores OO, e quase todo mundo pensa que variáveis ​​globais mutáveis ​​são uma má ideia. Línguas como Haskell apenas levam a ideia adiante que a maioria ...
CA McCann
5
@eSKay: Não é tanto que a mutação seja prejudicial, mas sim, se você concordar em evitar a mutação, torna-se muito mais fácil escrever um código modular e reutilizável. Sem o estado mutável compartilhado, o acoplamento entre diferentes partes do código se torna explícito e é muito mais fácil de entender e manter seu design. John Hughes explica isso melhor do que eu; pegue seu artigo Por que a programação funcional é importante .
Norman Ramsey