Costumo encontrar as seguintes declarações / argumentos:
- Linguagens de programação funcionais puras não permitem efeitos colaterais (e, portanto, são pouco úteis na prática, porque qualquer programa útil tem efeitos colaterais, por exemplo, quando interage com o mundo externo).
- Linguagens de programação funcionais puras não permitem escrever um programa que mantenha estado (o que torna a programação muito incômoda porque em muitos aplicativos você precisa de estado).
Eu não sou especialista em linguagens funcionais, mas aqui está o que eu entendi sobre esses tópicos até agora.
Em relação ao ponto 1, você pode interagir com o ambiente em linguagens puramente funcionais, mas é necessário marcar explicitamente o código (funções) que introduz efeitos colaterais (por exemplo, em Haskell por meio de tipos monádicos). Além disso, até onde sei a computação por efeitos colaterais (atualização destrutiva dos dados) também deve ser possível (usando tipos monádicos?), Mesmo que não seja a maneira preferida de trabalhar.
Em relação ao ponto 2, até onde eu sei, você pode representar o estado encadeando valores através de várias etapas de cálculo (em Haskell, novamente, usando tipos monádicos), mas não tenho experiência prática nisso e meu entendimento é bastante vago.
Então, as duas afirmações acima estão corretas em algum sentido ou são apenas conceitos errados sobre linguagens puramente funcionais? Se eles são conceitos errados, como eles surgiram? Você poderia escrever um trecho de código (possivelmente pequeno) que ilustra a maneira idiomática de Haskell de (1) implementar efeitos colaterais e (2) implementar uma computação com estado?
fonte
Respostas:
Para os fins desta resposta, defino "linguagem puramente funcional" como uma linguagem funcional na qual as funções são referencialmente transparentes, ou seja, chamar a mesma função várias vezes com os mesmos argumentos sempre produzirá os mesmos resultados. Essa é, acredito, a definição usual de uma linguagem puramente funcional.
A maneira mais fácil de obter transparência referencial seria de fato proibir efeitos colaterais e, de fato, existem idiomas nos quais esse é o caso (principalmente os específicos de domínio). No entanto, certamente não é a única maneira e as linguagens puramente funcionais de propósito geral (Haskell, Clean, ...) permitem efeito colateral.
Dizer também que uma linguagem de programação sem efeitos colaterais é pouco usada na prática não é realmente justo, eu acho - certamente não para linguagens específicas de domínio, mas mesmo para linguagens de uso geral, eu imagino que uma linguagem possa ser bastante útil sem fornecer efeitos colaterais . Talvez não para aplicativos de console, mas acho que aplicativos de GUI podem ser bem implementados sem efeitos colaterais, digamos, no paradigma reativo funcional.
Isso é um pouco mais do que simplificá-lo. Apenas ter um sistema em que as funções de efeito colateral precisam ser marcadas como tal (semelhante à const-correção no C ++, mas com efeitos colaterais gerais) não é suficiente para garantir a transparência referencial. Você precisa garantir que um programa nunca possa chamar uma função várias vezes com os mesmos argumentos e obter resultados diferentes. Você poderia fazer isso criando coisas como
readLine
seja algo que não seja uma função (é o que Haskell faz com a mônada de IO) ou você pode tornar impossível chamar funções de efeito colateral várias vezes com o mesmo argumento (é o que o Clean faz). No último caso, o compilador garantirá que toda vez que você chamar uma função de efeito colateral, faça isso com um argumento novo e rejeite qualquer programa em que você passe o mesmo argumento para uma função de efeito colateral duas vezes.Novamente, uma linguagem puramente funcional pode muito bem impedir o estado mutável, mas certamente é possível ser puro e ainda ter um estado mutável, se você implementá-lo da mesma maneira que eu descrevi com os efeitos colaterais acima. O estado realmente mutável é apenas outra forma de efeitos colaterais.
Dito isto, as linguagens de programação funcional definitivamente desencorajam o estado mutável - especialmente os puros. E não acho que isso torne a programação estranha - muito pelo contrário. Às vezes (mas nem sempre), o estado mutável não pode ser evitado sem perder o desempenho ou a clareza (é por isso que idiomas como Haskell têm recursos para o estado mutável), mas na maioria das vezes pode.
Eu acho que muitas pessoas simplesmente leem "uma função deve produzir o mesmo resultado quando chamada com os mesmos argumentos" e concluem que não é possível implementar algo como
readLine
ou código que mantenha um estado mutável. Portanto, eles simplesmente não estão cientes dos "truques" que as linguagens puramente funcionais podem usar para introduzir essas coisas sem quebrar a transparência referencial.Além disso, o estado mutável desencoraja fortemente as linguagens funcionais; portanto, não é um grande salto presumir que não seja permitido em todas as que sejam puramente funcionais.
Aqui está um aplicativo em Pseudo-Haskell que solicita um nome ao usuário e o cumprimenta. Pseudo-Haskell é uma linguagem que acabei de inventar, que possui o sistema de IO de Haskell, mas usa sintaxe mais convencional, nomes de função mais descritivos e sem
do
notação (pois isso apenas distrairia o modo como a mônada de IO funciona):A pista aqui é que
readLine
é um valor do tipoIO<String>
ecomposeMonad
é uma função que aceita um argumento do tipoIO<T>
(para algum tipoT
) e outro argumento que é uma função que pega um argumento do tipoT
e retorna um valor do tipoIO<U>
(para algum tipoU
).print
é uma função que recebe uma string e retorna um valor do tipoIO<void>
.Um valor do tipo
IO<A>
é um valor que "codifica" uma determinada ação que produz um valor do tipoA
.composeMonad(m, f)
produz um novoIO
valor que codifica a ação dem
seguida pela ação def(x)
, ondex
é o valor produzido executando a ação dem
.O estado mutável ficaria assim:
Aqui
mutableVariable
está uma função que pega valor de qualquer tipoT
e produz aMutableVariable<T>
. A funçãogetValue
pegaMutableVariable
e retorna umIO<T>
que produz seu valor atual.setValue
pega aMutableVariable<T>
e aT
e retorna umIO<void>
que define o valor.composeVoidMonad
é o mesmo quecomposeMonad
exceto que o primeiro argumento éIO
aquele que não produz um valor sensível e o segundo argumento é outra mônada, não uma função que retorna uma mônada.Em Haskell, há um pouco de açúcar sintático, que torna todo esse calvário menos doloroso, mas ainda é óbvio que o estado mutável é algo que a linguagem realmente não quer que você faça.
fonte
counter
, ou sejaincreaseCounter(counter)
?main
será a que realmente for executada. Além de retornar um IO demain
lá, não há maneira de executarIO
ações (sem usar funções terrivelmente ruins que têmunsafe
em seu nome).IO
valores destrutivos . Não entendi se ele se refere à correspondência de padrões, ou seja, ao fato de que você pode desconstruir valores de um tipo de dados algébricos, mas não se pode usar a correspondência de padrões para fazer isso comIO
valores.IMHO você está confuso porque há uma diferença entre uma linguagem pura e uma função pura . Vamos começar com a função. Uma função é pura se (sempre com a mesma entrada) retornar sempre o mesmo valor e não causar efeitos colaterais observáveis. Exemplos típicos são funções matemáticas como f (x) = x * x. Agora considere uma implementação desta função. Seria puro na maioria das línguas, mesmo naquelas que não são geralmente consideradas linguagens funcionais puras, por exemplo, ML. Mesmo um método Java ou C ++ com esse comportamento pode ser considerado puro.
Então, o que é uma linguagem pura? A rigor, pode-se esperar que uma linguagem pura não permita expressar funções que não são puras. Vamos chamar isso de definição idealista de uma linguagem pura. Esse comportamento é altamente desejável. Por quê? Bem, o bom de um programa que consiste apenas em funções puras é que você pode substituir o aplicativo de função por seu valor sem alterar o significado do programa. Isso facilita muito o raciocínio sobre os programas, pois depois que você souber o resultado, poderá esquecer a maneira como ele foi computado. A pureza também pode permitir que o compilador execute certas otimizações agressivas.
E daí se você precisar de algum estado interno? Você pode imitar o estado em uma linguagem pura simplesmente adicionando o estado antes da computação como parâmetro de entrada e o estado após a computação como parte do resultado. Em vez de
Int -> Bool
você obter algo parecidoInt -> State -> (Bool, State)
. Você simplesmente torna a dependência explícita (que é considerada uma boa prática em qualquer paradigma de programação). BTW, existe uma mônada que é uma maneira particularmente elegante de combinar essas funções de imitação de estado em funções maiores de imitação de estado. Dessa forma, você definitivamente pode "manter o estado" em uma linguagem pura. Mas você precisa explicitar.Então, isso significa que posso interagir com o exterior? Afinal, um programa útil deve interagir com o mundo real para ser útil. Mas entrada e saída obviamente não são puras. Escrever um byte específico em um arquivo específico pode ser bom pela primeira vez. Mas executar exatamente a mesma operação uma segunda vez pode retornar um erro porque o disco está cheio. Claramente, não existe linguagem pura (no significado idealista) que possa gravar em um arquivo.
Então, estamos diante de um dilema. Queremos principalmente funções puras, mas alguns efeitos colaterais são absolutamente necessários e esses não são puros. Agora, uma definição realista de uma linguagem pura seria que deve haver alguns meios para separar as partes puras das outras partes. O mecanismo deve garantir que nenhuma operação impura se infiltre nas partes puras.
Em Haskell, isso é feito com o tipo de E / S. Você não pode destruir um resultado de E / S (sem mecanismos não seguros). Portanto, você só pode processar resultados de E / S com funções definidas no próprio módulo de E / S. Felizmente, existem combinadores muito flexíveis que permitem obter um resultado de IO e processá-lo em uma função, desde que essa função retorne outro resultado de IO. Este elemento de combinação é chamado ligação (ou
>>=
) e tem o tipoIO a -> (a -> IO b) -> IO b
. Se você generaliza esse conceito, chega à classe Mônada e o IO passa a ser uma instância dele.fonte
unsafe
seu nome) não atende à sua definição idealista. Não há funções impuras em Haskell (novamente ignorandounsafePerformIO
e co.).readFile
ewriteFile
sempre retornará o mesmoIO
valor, dados os mesmos argumentos. Por exemplo, os dois trechos de códigolet x = writeFile "foo.txt" "bar" in x >> x
ewriteFile "foo.txt" "bar" >> writeFile "foo.txt" "bar"
farão a mesma coisa.IO Something
? Nesse caso, é perfeitamente possível chamar uma função de E / S duas vezes com o mesmo argumento:putStrLn "hello" >> putStrLn "hello"
- aqui ambas chamam paraputStrLn
ter o mesmo argumento. Claro que isso não é um problema, porque, como eu disse anteriormente, as duas chamadas resultarão no mesmo valor de IO.writeFile "foo.txt" "bar"
não pode causar um erro porque a avaliação da chamada de função não executa a ação. Se você está dizendo que, no meu exemplo anterior, a versão comlet
apenas uma oportunidade de causar uma falha de IO, enquanto a versão semlet
duas, você está errado. Ambas as versões têm duas oportunidades para uma falha de E / S. Como alet
versão avalia a chamadawriteFile
apenas uma vez, enquanto a versão sem alet
avalia duas vezes, você pode ver que não importa com que frequência a função é chamada. Isso só importa quantas vezes o resultado ...putStrLn
função usa exatamente um argumento, que é do tipoString
. Se você não acredita em mim, olhe para o seu tipo:String -> IO ()
. Certamente não aceita nenhum argumento do tipoIO
- produz um valor desse tipo.