FUZxxl ответил на немедленный вопрос, но я хотел бы расширить его еще несколькими способами написания «writeStr», чтобы проиллюстрировать больше о монадах.
Как сказал Делнан в комментариях, вы также можете написать
writeStr [] = return ()
writeStr (x:xs) = putChar x >> writeStr xs
На самом деле это десагаратная версия нотации do. Оператор «>>» используется для последовательного монадического действия. На самом деле это специализированная версия оператора "bind", написанная ">> =". См. этот вопрос для более подробной информации.
Но когда вы смотрите на это, кажется, что все, что мы делаем, это применяем "putChar" к каждому элементу в списке аргументов. Для этого в Prelude уже есть функция map, поэтому мы можем написать:
writeStr xs = map putChar xs
Но когда ты попробуешь, это не сработает. Причина становится очевидной, если вы зайдете в GHCi и напечатаете это:
:type map putChar "Hello"
[IO ()]
Вы хотите одно действие "IO ()", но это дает вам их список. Вам нужна функция, которая превращает этот список действий ввода-вывода в одно действие ввода-вывода. К счастью, один существует. Прелюдия содержит эти две функции
sequence :: [IO a] -> IO [a]
sequence_ :: [IO a] -> IO ()
Первый предназначен для тех случаев, когда вам нужен список результатов, второй - для случаев, когда вы этого не хотите, например этот. (В этом ответе для ясности я буду давать специфичные для IO сигнатуры типов, но важно помнить, что все эти функции действительно работают для любой монады.)
Итак, теперь вы можете написать:
writeStr xs = sequence_ $ map putChar xs
Но есть способ сократить это. Напомним "." оператор, который связывает две функции вместе, и способ, которым Haskell «каррирует» аргументы функции? Мы можем переписать функцию выше как:
writeStr = sequence_ . map putChar
Этот стиль "без очков" поначалу выглядит и кажется очень странным; это делает «writeStr» больше похожим на константу, чем на функцию. Но это избавляет от необходимости отслеживать имена переменных вокруг кода, когда вы читаете его, и поэтому часто предпочитается. Это также намного короче и более читабельно, когда вы помещаете что-то сложное в качестве аргумента в «map» или аналогичные функции более высокого порядка.
Но мы можем пойти еще короче. Шаблон «sequence.map f» очень распространен, поэтому модуль «Control.Monad» определяет еще пару функций для его воплощения:
mapM :: (a -> IO b) -> [a] -> IO [b]
mapM f = sequence . map f
mapM_ :: (a -> IO b) -> [a] -> IO ()
mapM_ f = sequence_ . map f
Таким образом, вы можете наконец написать
writeStr = mapM_ putChar