Haskell: uma excelente linguagem procedural
(entropicthoughts.com)Tratando efeitos colaterais como valores de primeira classe
- Em Haskell, efeitos colaterais (por exemplo, geração de números aleatórios, saída etc.) são tratados como “valores de primeira classe (first class values)”
- Ou seja, a chamada de uma função que gera um efeito colateral, como
randomRIO(1, 6), não retorna diretamente um valor de resultado, mas sim um “objeto que descreve uma ação a ser executada em algum momento” - Esse objeto produzirá um valor aleatório quando for de fato executado, mas antes disso contém apenas o plano de execução
- Um tipo como
IO Intrepresenta “uma ação que, quando realmente executada, produzirá um Int”; ela não é executada imediatamente no momento da chamada, e sim depois, quando necessário - Por causa dessa característica, ao contrário das linguagens procedurais tradicionais em que “chamada de função = execução imediata”, em Haskell é possível combinar efeitos colaterais e executá-los de verdade mais tarde
Desmistificando blocos do
- O bloco
donão é uma sintaxe mágica; na prática, ele é composto por dois operadores: um para encadear (bind) efeitos colaterais e outro para executá-los em sequência (then)
then
- O operador
*>executa o efeito colateral da esquerda, descarta o valor de resultado e em seguida executa o efeito colateral da direita - Por exemplo,
putStr "hello" *> putStrLn "world"cria uma única açãoIO ()que combina as duas saídas em sequência - Quando você escreve várias linhas em um bloco
do, internamente essa operação de execução sequencial é usada
bind
- O operador
>>=executa o efeito colateral da esquerda e passa o valor obtido para a função da direita - Ex.:
randomRIO(1, 6) >>= print_sidecria um efeito colateral que envia o resultado do dado paraprint_sidee o imprime - No bloco
do, o padrão<-é uma forma conveniente de expressar esse operador
Dois operadores são tudo que há em blocos do
- No fim das contas, o bloco
doé construído a partir desses dois operadores:*>e>>= - A sintaxe
doé muito usada por legibilidade e praticidade, mas para aproveitar melhor as vantagens de Haskell é importante usar também funções mais ricas de combinação de efeitos colaterais
Funções que operam sobre efeitos colaterais
- A biblioteca padrão traz várias funções para lidar com efeitos colaterais de maneiras mais diversas
pure
pure xcria “uma ação que produz o valor x como resultado sem nenhum efeito colateral adicional”- Ex.:
loaded_die = pure 4cria umIO Intque sempre retorna 4
fmap
- Na forma
fmap :: (a -> b) -> IO a -> IO b, ela cria uma ação que produz um novo valor de resultado aplicando uma função pura ao valor resultante de um efeito colateral - Ex.: em
length <$> getEnv "HOME", é possível aplicarlengthao efeito colateral que obtém a variável de ambiente e assim criar uma ação que calcula seu tamanho
liftA2, liftA3, …
- Funções como
liftA2eliftA3combinam os resultados de vários efeitos colaterais com uma única função pura para criar um novo efeito colateral - Ex.:
liftA2 (+) (randomRIO(1,6)) (randomRIO(1,6))cria um efeito colateral que soma os valores de dois dados - O mesmo pode ser feito com a combinação de
<$>e<*>
Interlúdio: qual é a vantagem?
- Isso pode parecer apenas uma funcionalidade simples que também seria possível em outras linguagens, mas em Haskell há a vantagem de que você pode extrair uma ação com efeito colateral para uma variável ou recombiná-la a qualquer momento sem alterar o momento de execução nem o resultado
- Ao tratar efeitos colaterais de forma independente, há menos confusão durante refatorações, e torna-se possível um reuso seguro baseado em raciocínio equacional (equational reasoning)
sequenceA
sequenceA [IO a] -> IO [a]transforma “uma lista de ações com efeitos colaterais” em “uma única ação com efeito colateral que produz uma lista de resultados”- Ex.: é possível reunir várias ações de
logem uma lista e depois executá-las de uma vez comsequenceA - Até mesmo efeitos colaterais infinitamente repetidos (ex.:
repeat (randomRIO(1,6))) podem ser guardados em uma lista, e então você usatake npara pegar só a quantidade necessária e executá-los comsequenceA
Interlúdio: funções de conveniência
void,sequenceA_,replicateM,replicateM_etc. são convenientes quando o valor de resultado não é usado ou quando se quer repetir uma execução- Ex.:
replicateM_ 500 (putStrLn "I will not cheat again.")permite executar várias vezes um efeito colateral sem contar manualmente o número de repetições
traverse
traverse :: (a -> IO b) -> [a] -> IO [b]cria uma ação que aplica uma função com efeito colateral a cada elemento da lista e reúne os resultados em uma listasequenceA, na verdade, é equivalente atraverse id, etraverse_é a versão que descarta os resultados
for
-
fortem a mesma função quetraverse, mas recebe os argumentos na ordem inversa -
Ex.: a forma
for numbers $ \n -> ...permite expressar naturalmente algo parecido com um “laço for” -
Graças a essas combinações, repetições, iterações e transformações de estruturas de dados que em outras linguagens exigiriam sintaxe específica podem ser implementadas em Haskell por meio da composição de funções de biblioteca
Explorando o fato de efeitos serem de primeira classe
- Em Haskell, aproveitar ativamente os efeitos colaterais como valores de primeira classe pode reduzir duplicação de código e melhorar a estrutura
- Por exemplo, em uma lógica de fatoração de números grandes com cache, é possível usar
Stateno lugar deIOpara criar uma estrutura em que “há efeitos colaterais, mas sem impacto externo” - Efeitos colaterais estruturados dessa forma são aplicados apenas onde são necessários, enquanto o restante do código pode permanecer como função pura, garantindo ao mesmo tempo segurança e flexibilidade
- No fim, com
evalStatee afins, é possível executar o efeito colateral real e transformar o resultado em um valor puro
Coisas com que você nunca precisa se preocupar
- Vários nomes herdados de épocas antigas do Haskell (
>>,return,mapMetc.) podem ser substituídos pelas funções atuais (*>,pure,traverseetc.) - Eles vêm de “nomes antigos ou de um design centrado em monads”; hoje em dia costuma-se recomendar uma abordagem baseada em Applicative ou no Functor mais geral
Apêndice A: evitando o sucesso e a inutilidade
- A frase “Haskell avoids success” significa que “a linguagem não sacrifica seus valores fundamentais em nome de popularidade ou conveniência”
- “Haskell is useless” remete ao contexto de que, no começo, por permitir apenas funções puras completas, parecia uma linguagem que realmente não podia fazer nada; depois, com a introdução da abordagem de tratar efeitos colaterais como algo ‘de primeira classe’, ela ganhou utilidade prática
Apêndice B: por que fmap funciona tanto sobre efeitos colaterais quanto sobre listas
fmaptem uma forma muito geral (Functor f => (a -> b) -> f a -> f b) e por isso pode ser aplicada em comum a vários contêineres ou tipos com efeitos colaterais, como listas, Maybe e IO- Quando se aplica
fmapa uma lista, a função é aplicada a todos os elementos; quando se aplica aIO, a função é aplicada ao valor de resultado - De forma geral, toda “estrutura à qual é possível aplicar uma função” é chamada de Functor
Apêndice C: Foldable e Traversable
Foldableé uma estrutura cujos elementos podem ser percorridos e processadosTraversableé uma estrutura que, além de ser percorrida, pode ser reconstruída com a mesma forma usando novos elementos- Para que
sequenceAoutraversepossam reunir valores preservando a estrutura original, essa estrutura precisa serTraversable - Estruturas de dados como árvores ou Set podem ter sua forma alterada conforme os valores, então distingue-se entre casos em que só é possível percorrer (
Foldable) e casos em que também é possível reconstruir de fato a estrutura (Traversable) - Dependendo da necessidade, também é possível tratar efeitos colaterais de forma flexível convertendo antes para uma lista e usando
traverse
2 comentários
Vejo muita propaganda sobre isso no Reddit... mas o próprio nome já cria uma certa barreira psicológica.
Dá a impressão de ser uma linguagem bem difícil e poderosa...
Comentários do Hacker News
O sistema de tipos do Haskell tem certa complexidade quando comparado a outras linguagens populares. Em especial, operadores como
*>,<*>e<*aumentam a curva de aprendizado em toda a base de código>>=e>>para manter a produtividadeHaskell ajuda a melhorar a programação imperativa
A versão generalizada de
traverse/mapMfunciona não só para listas, mas para qualquer tipoTraversable, e é muito útiltraverse :: Applicative f => (a -> f b) -> t a -> f (t b)Haskell tem mônadas poderosas, o que o torna mais procedural
doUm software escrito em Haskell é o ImplicitCAD
O código em Haskell pode ser lido como o de uma linguagem procedural, mas oferece vantagens ao trabalhar com funções com efeitos colaterais
>>é o nome antigo de<i>>, e os dois operadores têm associatividade à esquerda>>é definido comoinfixl 1e<i>>comoinfixl 4, então<i>>se associa com mais força do que>>IO aeaem Haskell podem passar uma sensação parecida com assíncrono e síncronoEm outras linguagens, é possível fazer IO simples com uma função como
console.log("abc")Quem nunca tentou usar Haskell pode achar que o Haskell real, com extensões do GHC, é complexo demais