Зачем нам нужны монады?

По моему скромному мнению, ответы на известный вопрос "Что такое монада?" особенно те, за кого проголосовали, попытайтесь объяснить, что такое монада, не объясняя, почему монады действительно необходимы. Могут ли они быть объяснены как решение проблемы?

7 ответов

Решение

Зачем нам нужны монады?

  1. Мы хотим программировать только с использованием функций. ("Функциональное программирование (ФП)" в конце концов).
  2. Тогда у нас есть первая большая проблема. Это программа:

    f(x) = 2 * x

    g(x,y) = x / y

    Как мы можем сказать, что должно быть выполнено первым? Как мы можем сформировать упорядоченную последовательность функций (то есть программу), используя не более чем функции?

    Решение: составить функции. Если хочешь первым g а потом f, просто пиши f(g(x,y)), Таким образом, "программа" также является функцией: main = f(g(x,y)), Да, но...

  3. Больше проблем: некоторые функции могут не работать (т.е. g(2,0), разделите на 0). У нас нет "исключений" в FP (исключение не является функцией). Как мы это решаем?

    Решение: давайте позволим функциям возвращать два вида вещей: вместо g : Real,Real -> Real (функция из двух реалов в реальный), давайте позволим g : Real,Real -> Real | Nothing (функция из двух реалов в (реальные или ничего)).

  4. Но функции должны (быть проще) возвращать только одну вещь.

    Решение: давайте создадим новый тип данных, которые должны быть возвращены, "тип бокса", который может быть реальным или просто ничем. Следовательно, мы можем иметь g : Real,Real -> Maybe Real, Да, но...

  5. Что происходит сейчас с f(g(x,y))? f не готов потреблять Maybe Real, И мы не хотим менять каждую функцию, с которой мы могли бы соединиться g потреблять Maybe Real,

    Решение: у нас есть специальная функция для "соединения"/"создания"/"связывания" функций. Таким образом, мы можем за кадром адаптировать вывод одной функции для передачи следующей.

    В нашем случае: g >>= f (Подключение / сочинить g в f). Мы хотим >>= получить gвывод, проверьте его и, если это Nothing просто не звони f и вернуться Nothing; или наоборот, распаковать в штучной упаковке Real и кормить f с этим. (Этот алгоритм является просто реализацией >>= для Maybe тип). Также обратите внимание, что >>= должен быть записан только один раз для "типа бокса" (другой бокс, другой алгоритм адаптации).

  6. Возникают многие другие проблемы, которые могут быть решены с использованием этого же шаблона: 1. Используйте "коробку" для кодификации / хранения различных значений / значений и используйте такие функции, как g которые возвращают эти "коробочные значения". 2. Есть композитор / компоновщик g >>= f чтобы помочь соединению gвыход в fвход, поэтому нам не нужно менять какие-либо f совсем.

  7. Замечательные проблемы, которые могут быть решены с помощью этой техники:

    • имея глобальное состояние, которое каждая функция в последовательности функций ("программа") может совместно использовать: решение StateMonad,

    • Нам не нравятся "нечистые функции": функции, которые дают разные выходные данные для одного и того же ввода. Поэтому давайте отметим эти функции, заставив их возвращать теговое / коробочное значение: IO монада.

Всего счастья!

Ответ, конечно, "мы не делаем". Как и во всех абстракциях, это не обязательно.

Haskell не нуждается в абстракции монады. Это не обязательно для выполнения ввода-вывода на чистом языке. IO Тип позаботится об этом просто отлично сам по себе. Существующая монадическая десугаринг do блоки могут быть заменены bindIO, returnIO, а также failIO как определено в GHC.Base модуль. (Это не документированный модуль по взлому, поэтому мне придется указать на его источник документации). Так что нет, абстракция монады не нужна.

Так что, если это не нужно, почему оно существует? Потому что было обнаружено, что многие модели вычислений образуют монадические структуры. Абстракция структуры позволяет писать код, который работает во всех экземплярах этой структуры. Короче говоря - повторное использование кода.

В функциональных языках самым мощным инструментом для повторного использования кода была композиция функций. Старый добрый (.) :: (b -> c) -> (a -> b) -> (a -> c) Оператор чрезвычайно мощный. Это позволяет легко писать крошечные функции и склеивать их с минимальными синтаксическими или семантическими издержками.

Но бывают случаи, когда типы работают не совсем правильно. Что вы делаете, когда у вас есть foo :: (b -> Maybe c) а также bar :: (a -> Maybe b)? foo . bar не проверять, потому что b а также Maybe b не того же типа.

Но.. Это почти правильно. Вы просто хотите немного свободы. Вы хотите быть в состоянии лечить Maybe b как будто это было в основном b, Это плохая идея - просто относиться к ним как к одному и тому же типу. Это более или менее то же самое, что нулевые указатели, которые Тони Хоар, как известно, назвал ошибкой в ​​миллиард долларов. Поэтому, если вы не можете относиться к ним как к одному типу, возможно, вы сможете найти способ расширить механизм композиции. (.) обеспечивает.

В этом случае важно действительно изучить теорию, лежащую в основе (.), К счастью, кто-то уже сделал это для нас. Оказывается, что сочетание (.) а также id сформировать математическую конструкцию, известную как категория. Но есть и другие способы формирования категорий. Например, категория Клейсли позволяет немного дополнить составляемые объекты. Клейсли категории для Maybe будет состоять из (.) :: (b -> Maybe c) -> (a -> Maybe b) -> (a -> Maybe c) а также id :: a -> Maybe a, То есть объекты в категории увеличивают (->) с Maybe, так (a -> b) становится (a -> Maybe b),

И вдруг, мы расширили силу композиции на вещи, которые традиционные (.) операция не работает Это источник новой силы абстракции. Категории Kleisli работают с большим количеством типов, чем просто Maybe, Они работают с каждым типом, который может собрать правильную категорию, подчиняясь законам категории.

  1. Левая личность: id . f знак равно f
  2. Правильная идентичность: f . id знак равно f
  3. Ассоциативность: f . (g . h) знак равно (f . g) . h

Пока вы можете доказать, что ваш тип подчиняется этим трем законам, вы можете превратить его в категорию Клейсли. И что в этом такого? Что ж, получается, что монады - это то же самое, что и категории Клейсли. Monad"s return такой же, как Клейсли id, Monad"s (>>=) не идентичен Клейсли (.), но оказывается, что очень легко написать каждый с точки зрения другого. И законы категорий такие же, как законы монад, когда вы переводите их через разницу между (>>=) а также (.),

Так зачем переживать все это? Почему есть Monad абстракция на языке? Как я упоминал выше, это позволяет повторно использовать код. Он даже позволяет повторно использовать код в двух разных измерениях.

Первое измерение повторного использования кода происходит непосредственно от наличия абстракции. Вы можете написать код, который работает во всех случаях абстракции. Существует целый пакет monad-loops, состоящий из циклов, которые работают с любым экземпляром Monad,

Второе измерение является косвенным, но оно вытекает из существования композиции. Когда композиция проста, естественно писать код небольшими, многократно используемыми кусками. Это так же, как (.) Оператор для функций поощряет написание небольших, многократно используемых функций.

Так почему же существует абстракция? Потому что доказано, что это инструмент, который обеспечивает большую композицию в коде, что приводит к созданию кода многократного использования и стимулирует создание кода многократного использования. Повторное использование кода является одним из святых Граалей программирования. Абстракция монады существует потому, что она немного подталкивает нас к этому святому Граалю.

Бенджамин Пирс сказал в TAPL

Систему типов можно рассматривать как вычисление своего рода статического приближения к поведению терминов в программе во время выполнения.

Вот почему язык, оснащенный мощной системой типов, строго более выразителен, чем плохо типизированный язык. Вы можете думать о монадах таким же образом.

Как указывают @Carl и sigfpe, вы можете оборудовать тип данных всеми нужными вам операциями, не прибегая к монадам, классам типов или любым другим абстрактным вещам. Однако монады позволяют вам не только писать повторно используемый код, но и абстрагироваться от всех лишних деталей.

В качестве примера, скажем, мы хотим отфильтровать список. Самый простой способ - это использовать filter функция: filter (> 3) [1..10], который равен [4,5,6,7,8,9,10],

Немного более сложная версия filter, который также проходит аккумулятор слева направо, является

swap (x, y) = (y, x)
(.*) = (.) . (.)

filterAccum :: (a -> b -> (Bool, a)) -> a -> [b] -> [b]
filterAccum f a xs = [x | (x, True) <- zip xs $ snd $ mapAccumL (swap .* f) a xs]

Чтобы получить все i такой, что i <= 10, sum [1..i] > 4, sum [1..i] < 25 мы можем написать

filterAccum (\a x -> let a' = a + x in (a' > 4 && a' < 25, a')) 0 [1..10]

что равно [3,4,5,6],

Или мы можем переопределить nub функция, которая удаляет повторяющиеся элементы из списка, с точки зрения filterAccum:

nub' = filterAccum (\a x -> (x `notElem` a, x:a)) []

nub' [1,2,4,5,4,3,1,8,9,4] равняется [1,2,4,5,3,8,9], Список передается здесь как аккумулятор. Код работает, потому что можно оставить монаду списка, поэтому все вычисления остаются чистыми (notElem не использует >>= на самом деле, но это может). Однако невозможно безопасно покинуть монаду ввода-вывода (то есть вы не можете выполнить действие ввода-вывода и вернуть чистое значение - значение всегда будет заключено в монаду ввода-вывода). Другой пример - изменяемые массивы: после того, как вы оставили монаду ST, в которой находится изменяемый массив, вы больше не можете обновлять массив в постоянное время. Итак, нам нужна монадическая фильтрация из Control.Monad модуль:

filterM          :: (Monad m) => (a -> m Bool) -> [a] -> m [a]
filterM _ []     =  return []
filterM p (x:xs) =  do
   flg <- p x
   ys  <- filterM p xs
   return (if flg then x:ys else ys)

filterM выполняет монадическое действие для всех элементов списка, получая элементы, для которых монадическое действие возвращает True,

Пример фильтрации с массивом:

nub' xs = runST $ do
        arr <- newArray (1, 9) True :: ST s (STUArray s Int Bool)
        let p i = readArray arr i <* writeArray arr i False
        filterM p xs

main = print $ nub' [1,2,4,5,4,3,1,8,9,4]

печать [1,2,4,5,3,8,9] как и ожидалось.

И версия с монадой ввода / вывода, которая спрашивает, какие элементы возвращать:

main = filterM p [1,2,4,5] >>= print where
    p i = putStrLn ("return " ++ show i ++ "?") *> readLn

Например

return 1? -- output
True      -- input
return 2?
False
return 4?
False
return 5?
True
[1,5]     -- output

И как последняя иллюстрация, filterAccum можно определить с точки зрения filterM:

filterAccum f a xs = evalState (filterM (state . flip f) xs) a

с StateT Монада, которая используется под капотом, является обычным типом данных.

Этот пример иллюстрирует, что монады не только позволяют абстрагировать вычислительный контекст и писать чистый повторно используемый код (благодаря сочетаемости монад, как объясняет @Carl), но также одинаково обрабатывать пользовательские типы данных и встроенные примитивы.

Я не думаю IO следует рассматривать как особенно выдающуюся монаду, но она, безусловно, одна из самых поразительных для начинающих, поэтому я буду использовать ее для своего объяснения.

Наивное построение системы ввода-вывода для Haskell

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

main₀ :: String -> String
main₀ _ = "Hello World"

С ленивостью этой простой подписи достаточно для создания интерактивных терминальных программ - хотя и очень ограниченных. Больше всего расстраивает то, что мы можем только выводить текст. Что если мы добавим еще несколько захватывающих возможностей вывода?

data Output = TxtOutput String
            | Beep Frequency

main₁ :: String -> [Output]
main₁ _ = [ TxtOutput "Hello World"
          -- , Beep 440  -- for debugging
          ]

мило, но, конечно, гораздо более реалистичный "альтернативный вывод" будет записывать в файл. Но тогда вам также понадобится способ чтения из файлов. Любой шанс?

Ну, когда мы берем наш main₁ Программируя и просто направляя файл в процесс (используя средства операционной системы), мы по существу реализовали чтение файла. Если бы мы могли запустить чтение файлов из языка Haskell...

readFile :: Filepath -> (String -> [Output]) -> [Output]

Это будет использовать "интерактивную программу" String->[Output], передайте ему строку, полученную из файла, и получите неинтерактивную программу, которая просто выполняет данную.

Здесь есть одна проблема: у нас нет понятия о том, когда файл читается. [Output] list конечно дает хороший порядок для выходных данных, но мы не получаем порядок для того, когда будут выполнены входные данные.

Решение: сделать входные события также пунктами в списке дел.

data IO₀ = TxtOut String
         | TxtIn (String -> [Output])
         | FileWrite FilePath String
         | FileRead FilePath (String -> [Output])
         | Beep Double

main₂ :: String -> [IO₀]
main₂ _ = [ FileRead "/dev/null" $ \_ ->
             [TxtOutput "Hello World"]
          ]

Хорошо, теперь вы можете заметить дисбаланс: вы можете прочитать файл и сделать вывод зависимым от него, но вы не можете использовать содержимое файла, чтобы решить, например, также прочитать другой файл. Очевидное решение: сделать результат ввода-события также чем-то типа IO, не просто Output, Это, конечно, включает в себя простой вывод текста, но также позволяет читать дополнительные файлы и т.д..

data IO₁ = TxtOut String
         | TxtIn (String -> [IO₁])
         | FileWrite FilePath String
         | FileRead FilePath (String -> [IO₁])
         | Beep Double

main₃ :: String -> [IO₁]
main₃ _ = [ TxtIn $ \_ ->
             [TxtOut "Hello World"]
          ]

Теперь это фактически позволяет вам выражать любую файловую операцию, которую вы можете захотеть в программе (хотя, возможно, не с хорошей производительностью), но это несколько усложняет:

  • main₃ выдает полный список действий. Почему бы нам просто не использовать подпись :: IO₁, который имеет это как особый случай?

  • Списки больше не дают надежного обзора хода выполнения программы: большинство последующих вычислений будут "объявлены" только в результате некоторой операции ввода. Таким образом, мы могли бы также отказаться от структуры списка и просто заключить "и затем сделать" в каждую операцию вывода.

data IO₂ = TxtOut String IO₂
         | TxtIn (String -> IO₂)
         | Terminate

main₄ :: IO₂
main₄ = TxtIn $ \_ ->
         TxtOut "Hello World"
          Terminate

Не так уж плохо!

Так какое отношение все это имеет к монадам?

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

getTime :: (UTCTime -> IO₂) -> IO₂
randomRIO :: Random r => (r,r) -> (r -> IO₂) -> IO₂
findFile :: RegEx -> (Maybe FilePath -> IO₂) -> IO₂

Здесь, очевидно, есть закономерность, и нам лучше написать

type IO₃ a = (a -> IO₂) -> IO₂    -- If this reminds you of continuation-passing
                                  -- style, you're right.

getTime :: IO₃ UTCTime
randomRIO :: Random r => (r,r) -> IO₃ r
findFile :: RegEx -> IO₃ (Maybe FilePath)

Теперь это начинает казаться знакомым, но мы все еще имеем дело только с тонко замаскированными простыми функциями под капотом, и это рискованно: каждое "действие-значение" несет ответственность за фактическую передачу результирующего действия любой содержащейся функции (иначе поток управления всей программой легко нарушается одним плохим поведением в середине). Нам лучше сделать это требование явным. Что ж, получается, что это законы монады, хотя я не уверен, что мы сможем сформулировать их без стандартных операторов связывания / объединения.

Во всяком случае, теперь мы достигли формулировки ввода-вывода, которая имеет надлежащий экземпляр монады:

data IO₄ a = TxtOut String (IO₄ a)
           | TxtIn (String -> IO₄ a)
           | TerminateWith a

txtOut :: String -> IO₄ ()
txtOut s = TxtOut s $ TerminateWith ()

txtIn :: IO₄ String
txtIn = TxtIn $ TerminateWith

instance Functor IO₄ where
  fmap f (TerminateWith a) = TerminateWith $ f a
  fmap f (TxtIn g) = TxtIn $ fmap f . g
  fmap f (TxtOut s c) = TxtOut s $ fmap f c

instance Applicative IO₄ where
  pure = TerminateWith
  (<*>) = ap

instance Monad IO₄ where
  TerminateWith x >>= f = f x
  TxtOut s c >>= f = TxtOut s $ c >>= f
  TxtIn g >>= f = TxtIn $ (>>=f) . g

Очевидно, что это не эффективная реализация ввода-вывода, но в принципе это удобно.

Монады служат в основном для объединения функций в цепочку. Период.

Теперь то, как они сочиняются, отличается в существующих монадах, что приводит к разным поведениям (например, для имитации изменяемого состояния в монаде состояний).

Путаница в отношении монад заключается в том, что, будучи настолько общим, то есть механизмом для составления функций, они могут использоваться для многих вещей, что приводит людей к убеждению, что монады имеют отношение к состоянию, к IO и т. Д., Когда речь идет только о "составлении функций ".

Теперь, одна интересная вещь о монадах, это то, что результат композиции всегда имеет тип "M a", то есть значение внутри конверта, помеченного "M". Эта особенность очень полезна для реализации, например, четкого разделения между чистым и нечистым кодом: объявляйте все нечистые действия как функции типа "IO a" и не предоставляйте никакой функции при определении монады IO для удаления " "значение изнутри"IO a". В результате ни одна функция не может быть чистой и в то же время извлекать значение из "IO a", потому что нет способа получить такое значение, оставаясь чистым (функция должна быть внутри монады "IO", чтобы использовать такая ценность). (ПРИМЕЧАНИЕ: ну, нет ничего идеального, поэтому "IO straitjacket" можно сломать с помощью "unsafePerformIO: IO a -> a", таким образом загрязняя то, что должно было быть чистой функцией, но это следует использовать очень экономно, а когда вы действительно знать, чтобы не вводить какой-либо нечистый код с побочными эффектами.

Монады - просто удобная структура для решения класса повторяющихся проблем. Во-первых, монады должны быть функторами (т.е. должны поддерживать отображение, не смотря на элементы (или их тип)), они также должны вызывать операцию привязки (или сцепления) и способ создания монадического значения из типа элемента (return). В заключение, bind а также return должны удовлетворять двум уравнениям (левой и правой тождествам), также называемым законами монады. (В качестве альтернативы можно определить монады, чтобы иметь flattening operation вместо привязки.)

Монада списка обычно используется для борьбы с недетерминизмом. Операция связывания выбирает один элемент списка (все они интуитивно понятны в параллельных мирах), позволяет программисту выполнить некоторые вычисления с ними, а затем объединяет результаты всех миров в один список (путем объединения или сглаживания вложенного списка).). Вот как можно определить функцию перестановки в монадической структуре Haskell:

perm [e] = [[e]]
perm l = do (leader, index) <- zip l [0 :: Int ..]
            let shortened = take index l ++ drop (index + 1) l
            trailer <- perm shortened
            return (leader : trailer)

Вот пример сессии repl:

*Main> perm "a"
["a"]
*Main> perm "ab"
["ab","ba"]
*Main> perm ""
[]
*Main> perm "abc"
["abc","acb","bac","bca","cab","cba"]

Следует отметить, что монада списка никоим образом не является побочным эффектом вычислений. Математическая структура, являющаяся монадой (то есть соответствующей вышеупомянутым интерфейсам и законам), не подразумевает побочных эффектов, хотя побочные явления часто хорошо вписываются в монадическую структуру.

Вам нужны монады, если у вас есть конструктор типов и функции, которые возвращают значения этого семейства типов. В конце концов, вы хотели бы объединить эти функции вместе. Вот три ключевых элемента, чтобы ответить, почему.

Позвольте мне уточнить. У тебя есть Int, String а также Real и функции типа Int -> String, String -> Real и так далее. Вы можете легко комбинировать эти функции, заканчивая Int -> Real, Жизнь хороша.

Затем однажды вам нужно создать новое семейство типов. Это может быть потому, что вам нужно рассмотреть возможность возврата значения (Maybe), возвращая ошибку (Either), несколько результатов (List) и так далее.

Заметить, что Maybe это конструктор типа Требуется тип, как Int и возвращает новый тип Maybe Int, Первое, что нужно запомнить: нет конструктора типов, нет монады.

Конечно, вы хотите использовать ваш конструктор типов в вашем коде, и вскоре вы закончите с такими функциями, как Int -> Maybe String а также String -> Maybe Float, Теперь вы не можете легко комбинировать свои функции. Жизнь больше не хороша.

И вот когда монады приходят на помощь. Они позволяют вам снова комбинировать такие функции. Вам просто нужно изменить состав . для > ==.

Why do we need monads?

We don't.

From How to Declare an Imperative by Philip Wadler:

(* page 25 *)
val echoML    : unit -> unit
fun echoML () = let val c = getcML () in
                if c = #"\n" then
                  ()
                else
                  (putcML c; echoML ())
                end

where:

(* pages 25-26 *)
fun putcML c  = TextIO.output1(TextIO.stdOut,c);
fun getcML () = valOf(TextIO.input1(TextIO.stdIn));

Yes, alright - you're probably trying to learn Haskell, and that's why you eventually ended up here. As it happens, it was the quandary of I/O in nonstrict languages like Haskell that brought monads to such prominence - that's why I've chosen I/O for the running example.

Now, you can write echo in Haskell like this:

echoH :: IO ()
echoH =  do c <- getChar
            if c == '\n' then
              return ()
            else
              putChar c >> echoH

or this:

echoH' :: IO ()
echoH' =  getChar   >>= \c ->
          if c == '\n' then return () else
          putChar c >> echoH'

but you cannot write this:

errcho    :: () -> ()
errcho () =  let c = getc () in
             if c == '\n' then
               ()
             else
               putc c ; errcho ()

 -- fake primitives!
(;)  :: a -> b -> b
putc :: Char -> ()
getc :: ()   -> Char

That ain't legit Haskell...but this almost is:

echo   :: IO -> ()
echo u =  let !u1:u2:u3:_ = parts u in
          let !c          = getchar u1 in
          if c == '\n' then () else putchar c u2 `seq` echo u3

where:

data OI             -- abstract
parts :: OI -> [OI] -- primitive

 -- I'll leave these definitions to you ;-)
putchar :: Char -> OI -> ()
getchar :: OI -> Char

(Those bang-patterns are an extension of Haskell 2010, and seq isn't actually sequential.)

It's clunkier, but this is regular Haskell:

echo   :: IO -> ()
echo u =  case parts u of
            u1:u2:u3:_ -> case getchar u1 of
                            c -> if c == '\n' then () else
                                 putchar c u2 =- echo u3

For (=-), while some extensions are required:

 -- for GHC 8.6.5
{-# LANGUAGE CPP #-}
#define during seq
import qualified Prelude(during)

{-# NOINLINE (=-) #-}
infixr 0 =-
(=-) :: a -> b -> b
x =- y = Prelude.during x (case x of _ -> y)

they stay with its definition.

That didn't work? Try this:

 -- for GHC 8.6.5
{-# LANGUAGE CPP #-}
#define during seq
module Sequential(seq) where
import qualified Prelude(during)
import GHC.Base(lazy)

infixr 0 =-
=- :: a -> b -> b
x =- y = Prelude.during x (lazy y)

Yes, it's a bit arcane, but (=-), parts, and those odd OI values can allow you to do neat stuff like this:

runDialogue :: Dialogue -> OI -> ()    
runDialogue d =
    \u -> foldr (=-) () (yet (\l -> zipWith respond (d l) (parts u)))

respond :: Request -> OI -> Response
respond Getq     = getchar `bind` (unit . Getp)
respond (Putq c) = putchar c `bind` \_ -> unit Putp

where:

 -- types from page 14
type Dialogue = [Response] -> [Request]

data Request  = Getq | Putq Char
data Response = Getp Char | Putp

yet      :: (a -> a) -> a
yet f    =  f (yet f)

unit     :: a -> (OI -> a)
unit x   =  \u -> part u =- x

bind     :: (OI -> a) -> (a -> (OI -> b)) -> (OI -> b)
bind m k =  \u -> case part u of (u1, u2) -> (\x -> x =- k x u2) (m u1)

part     :: OI -> (OI, OI)
part u   =  case parts u of u1:u2:_ -> (u1, u2)

It isn't working? Give this a try:

yet      :: (a -> a) -> a
yet f    =  y where y = f y

Yes, continually typing out OI -> would be annoying, and if this approach to I/O is going to work, it has to work everywhere. The simplest solution is:

type IO a = OI -> a

to avoid the hassle of wrapping and unwrapping involved with using constructors. The change of type also provides main with an alternate type signature:

main :: OI -> ()

To conclude - while monads can be very useful:

echo' :: OI -> ()
echo' =  getchar   `bind` \c ->
         if c == '\n' then unit () else
         putchar c `bind` \_ -> echo'

we don't need them in Haskell.

Другие вопросы по тегам