Как соединить две монады Haskell IO

Oct 02 2020

Следующая (рабочая) программа на Haskell выводит случайное заклинание:

import System.Random

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

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

Однако переменная spellсовершенно бесполезна. Он сохраняет случайную строку, выбранную из списка заклинаний, но затем немедленно передается в putStrLnфункцию и больше никогда не используется. Я попытался объединить две операции ввода-вывода в одну строку следующим образом:

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

Но я получил следующую ошибку:

    • 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)
    |        ^^^^^^^^^^^^^^^^^^^^^^^^

Есть ли способ объединить две операции ввода-вывода в одну строку? Я посмотрел на этот похожий вопрос, но не смог понять ответа.

Ответы

7 amalloy Oct 02 2020 at 06:25

(>>=)является «каноническим» оператором монады, как указано в ответе Робина Зигмонда. Однако, если вы пытаетесь код записи в аппликативном-как стиль, я часто люблю использовать свою перевернутую версию, (=<<). Он имеет приятную симметрию с функциями в Functor и Applicative и тем, как они напоминают вызов обычной немонадической функции с добавлением только одного дополнительного оператора:

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?

Так что ваше выражение может быть написано

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

Лично я бы предпочел использовать более обычную композицию функций и менее контекстное сопоставление, поэтому я бы переместил (spells !!)левую часть оператора привязки:

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

Видите, как это хорошо читается в таком порядке? "Распечатать заклинание по индексу, заданному randomRIO (0, length spells - 1)"?

6 RobinZigmond Oct 02 2020 at 04:00

doнотация - это просто синтаксический сахар для использования оператора связывания ( >>=). Итак, ваш двухстрочный doблок можно переписать:

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 

Однако я бы спросил, есть ли какая-либо читаемость или другая выгода от этого.