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.
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).
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.
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 as apresenta (por exemplo, no Haskell por meio de tipos monádicos).
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 comoreadLine
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.
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).
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.
Se eles são conceitos errados, como eles surgiram?
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.
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?
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):
greet(name) = print("Hello, " ++ name ++ "!")
main = composeMonad(readLine, greet)
A pista aqui é que readLine
é um valor do tipo IO<String>
e composeMonad
é uma função que aceita um argumento do tipo IO<T>
(para algum tipo T
) e outro argumento que é uma função que pega um argumento do tipo T
e retorna um valor do tipo IO<U>
(para algum tipo U
). print
é uma função que recebe uma string e retorna um valor do tipo IO<void>
.
Um valor do tipo IO<A>
é um valor que "codifica" uma determinada ação que produz um valor do tipo A
. composeMonad(m, f)
produz um novo IO
valor que codifica a ação de m
seguida pela ação de f(x)
, onde x
é o valor produzido executando a ação de m
.
O estado mutável ficaria assim:
counter = mutableVariable(0)
increaseCounter(cnt) =
setIncreasedValue(oldValue) = setValue(cnt, oldValue + 1)
composeMonad(getValue(cnt), setIncreasedValue)
printCounter(cnt) = composeMonad( getValue(cnt), print )
main = composeVoidMonad( increaseCounter(counter), printCounter(counter) )
Aqui mutableVariable
está uma função que pega valor de qualquer tipo T
e produz a MutableVariable<T>
. A função getValue
pega MutableVariable
e retorna um IO<T>
que produz seu valor atual. setValue
pega a MutableVariable<T>
e a T
e retorna um IO<void>
que define o valor. composeVoidMonad
é o mesmo que composeMonad
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.