Como juntar duas mônadas Haskell IO

Oct 02 2020

O seguinte programa Haskell (funcionando) produz um feitiço aleatório:

import System.Random

spells =
  [ "Abracadabra!"
  , "Hocus pocus!"
  , "Simsalabim!"
  ]

main :: IO()
main = do
  spell <- (spells !!) <$> randomRIO (0, length spells - 1)
  putStrLn spell

No entanto, a variável spellé totalmente inútil. Ele armazena a sequência aleatória selecionada na lista de magias, mas é imediatamente passada para a putStrLnfunção e nunca é usada novamente. Tentei combinar as duas operações de IO em uma única linha, como esta:

main = putStrLn <$> (spells !!) <$> randomRIO (0, length spells - 1)

Mas recebi o seguinte erro:

    • Couldn't match type ‘IO ()’ with ‘()’
      Expected type: Int -> ()
        Actual type: Int -> IO ()
    • In the first argument of ‘(<$>)’, namely
        ‘putStrLn <$> (spells !!)’ In the expression: putStrLn <$> (spells !!) <$> randomRIO (0, length spells - 1) In an equation for ‘main’: main = putStrLn <$> (spells !!) <$> randomRIO (0, length spells - 1) | 160 | main = putStrLn <$> (spells !!) <$> randomRIO (0, length spells - 1)
    |        ^^^^^^^^^^^^^^^^^^^^^^^^

Existe uma maneira de combinar as duas operações de E / S em uma única linha? Eu olhei para esta pergunta semelhante, mas não consegui entender a resposta.

Respostas

7 amalloy Oct 02 2020 at 06:25

(>>=)é o operador de mônada "canônico", conforme dado na resposta de Robin Zigmond. No entanto, se você está tentando escrever código em um estilo de aplicativo, geralmente gosto de usar sua versão invertida (=<<),. Tem uma boa simetria com as funções em Functor e Applicative, e como elas se assemelham a uma chamada de função não monádica comum com apenas um operador extra interposto:

f x -- one-argument function call
f <$> fx -- fmapping that function into a functor g x y -- two-argument function call g <$> ax <*> ay -- applied over two applicatives
f =<< mx -- binding a function with a monadic value
mx >>= f -- looks backwards, doesn't it?

Então sua expressão pode ser escrita

main = putStrLn =<< (spells !!) <$> randomRIO (0, length spells - 1)

Pessoalmente, prefiro usar uma composição de função mais comum e menos mapeamento contextual, então movo o (spells !!)para a esquerda do operador de ligação:

main = putStrLn . (spells !!) =<< randomRIO (0, length spells - 1)

Viu como fica bem assim? "Imprimir o feitiço no índice fornecido por randomRIO (0, length spells - 1)"?

6 RobinZigmond Oct 02 2020 at 04:00

doa notação é apenas um açúcar sintático para uso do operador bind ( >>=). Portanto, seu dobloco de 2 linhas pode ser reescrito:

main = do
    spells <- (spells !!) <$> randomRIO (0, length spells - 1) putStrLn spells -- rewrite to bind notation main = ((spells !!) <$> randomRIO (0, length spells - 1)) >>= \spells ->
    putStrLn spells

-- simplify the lambda

main = ((spells !!) <$> randomRIO (0, length spells - 1)) >>= putStrLn 

No entanto, eu questionaria se há algum ganho de legibilidade, ou outro, fazendo isso aqui.