3 pontos por GN⁺ 2025-01-20 | 2 comentários | Compartilhar no WhatsApp

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 Int representa “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 do nã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ção IO () 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_side cria um efeito colateral que envia o resultado do dado para print_side e 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 x cria “uma ação que produz o valor x como resultado sem nenhum efeito colateral adicional”
  • Ex.: loaded_die = pure 4 cria um IO Int que 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 aplicar length ao 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 liftA2 e liftA3 combinam 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 log em uma lista e depois executá-las de uma vez com sequenceA
  • Até mesmo efeitos colaterais infinitamente repetidos (ex.: repeat (randomRIO(1,6))) podem ser guardados em uma lista, e então você usa take n para pegar só a quantidade necessária e executá-los com sequenceA

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 lista
  • sequenceA, na verdade, é equivalente a traverse id, e traverse_ é a versão que descarta os resultados

for

  • for tem a mesma função que traverse, 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 State no lugar de IO para 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 evalState e 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, mapM etc.) podem ser substituídos pelas funções atuais (*>, pure, traverse etc.)
  • 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

  • fmap tem 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 fmap a uma lista, a função é aplicada a todos os elementos; quando se aplica a IO, 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 processados
  • Traversable é uma estrutura que, além de ser percorrida, pode ser reconstruída com a mesma forma usando novos elementos
  • Para que sequenceA ou traverse possam reunir valores preservando a estrutura original, essa estrutura precisa ser Traversable
  • 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

 
bbulbum 2025-01-21

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...

 
GN⁺ 2025-01-20
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

    • Se você ficar um mês sem usar Haskell, pode precisar estudar novamente operadores como >>= e >> para manter a produtividade
    • É difícil estudar os conceitos de Haskell sozinho, sem conversar com outras pessoas sobre eles
  • Haskell ajuda a melhorar a programação imperativa

    • É possível eliminar código boilerplate usando efeitos de primeira classe e padrões
    • Com segurança de tipos, dá para escrever código relativamente livre de bugs com rapidez
  • A versão generalizada de traverse/mapM funciona não só para listas, mas para qualquer tipo Traversable, e é muito útil

    • Pode ser usada na forma traverse :: Applicative f => (a -> f b) -> t a -> f (t b)
    • Em outras linguagens, era preciso escrever muito código manualmente para obter um efeito parecido
  • Haskell tem mônadas poderosas, o que o torna mais procedural

    • É possível usar variáveis intermediárias em blocos do
  • Um 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

    • Trabalhar com a mônada IO é complexo, e fica ainda mais complexo quando se quer usar outros tipos de mônada
  • >> é o nome antigo de <i>>, e os dois operadores têm associatividade à esquerda

    • >> é definido como infixl 1 e <i>> como infixl 4, então <i>> se associa com mais força do que >>
  • IO a e a em Haskell podem passar uma sensação parecida com assíncrono e síncrono

    • O primeiro retorna uma promessa/futuro que precisa ser aguardada
  • Em outras linguagens, é possível fazer IO simples com uma função como console.log("abc")

    • Há dúvidas sobre se existe alguma diferença em relação ao IO do Haskell
  • Quem nunca tentou usar Haskell pode achar que o Haskell real, com extensões do GHC, é complexo demais

    • Isso pode reduzir o interesse por Haskell