Давайте сначала проведем различие между изучением абстрактных понятий и изучением конкретных примеров из них.
Вы не будете слишком далеко игнорировать все конкретные примеры по той простой причине, что они совершенно вездесущи. Фактически, абстракции существуют в значительной степени потому, что они объединяют то, что вы будете делать в любом случае с конкретными примерами.
С другой стороны, сами абстракции, безусловно, полезны , но в них нет необходимости. Вы можете довольно далеко проигнорировать абстракции полностью и просто использовать различные типы напрямую. Вы захотите понять их в конце концов, но вы всегда можете вернуться к этому позже. На самом деле, я почти гарантирую, что если вы сделаете это, когда вернетесь к этому, вы будете бить себя по лбу и удивляться, почему вы потратили все это время на трудный путь, вместо того, чтобы использовать удобные инструменты общего назначения.
Взять хотя Maybe a
бы пример. Это просто тип данных:
data Maybe a = Just a | Nothing
Это все, кроме самодокументирования; это необязательное значение. Либо у вас «просто» что-то типа a
, либо у вас ничего нет. Допустим, у вас есть какая-то функция поиска, которая возвращает Maybe String
представление для поиска String
значения, которое может отсутствовать. Таким образом, вы сопоставляете шаблон со значением, чтобы увидеть, какое оно:
case lookupFunc key of
Just val -> ...
Nothing -> ...
Это все!
На самом деле, вам больше ничего не нужно. Нет Functor
s или Monad
s или что-нибудь еще. Они выражают общие способы использования Maybe a
ценностей ... но это просто идиомы, "шаблоны проектирования", как бы вы это ни называли.
Единственное место, где вы действительно не можете избежать этого IO
, - это таинственный черный ящик, так что не стоит пытаться понять, что это значит как-то Monad
или что-то еще.
На самом деле, вот шпаргалка для всего, что вам действительно нужно знать IO
на данный момент:
Если у чего-то есть тип IO a
, это означает, что это процедура , которая что-то делает и выплевывает a
значение.
Когда у вас есть блок кода с использованием do
нотации, напишите что-то вроде этого:
do -- ...
inp <- getLine
-- etc...
... означает выполнить процедуру справа от <-
и назначить результат имени слева.
Тогда как если у вас есть что-то вроде этого:
do -- ...
let x = [foo, bar]
-- etc...
... это означает присвоение значения простого выражения (не процедуры) справа от =
имени имени слева.
Если вы поместите что-то туда без присвоения значения, вот так:
do putStrLn "blah blah, fishcakes"
... это означает выполнение процедуры и игнорирование всего, что она возвращает. Некоторые процедуры имеют тип IO ()
- ()
тип является своего рода заполнителем, который ничего не говорит, так что это просто означает, что процедура что-то делает и не возвращает значение. Вроде какvoid
функция в других языках.
Выполнение одной и той же процедуры более одного раза может дать разные результаты; это своего рода идея. Вот почему нет способа «удалить» значение IO
из значения, потому что что-то IO
не является значением, это процедура для получения значения.
Последняя строка в do
блоке должна быть простой процедурой без присваивания, где возвращаемое значение этой процедуры становится возвращаемым значением для всего блока. Если вы хотите, чтобы возвращаемое значение использовало какое-то уже присвоенное значение, return
функция принимает простое значение и дает вам неоперативную процедуру, которая возвращает это значение.
В этом нет ничего особенного IO
; эти процедуры на самом деле представляют собой простые значения, и вы можете передавать их и комбинировать по-разному. Только когда они выполняются в do
блоке, вызванном где-то main
, они делают что-либо.
Итак, в чем-то вроде этого совершенно скучного, стереотипного примера программы:
hello = do putStrLn "What's your name?"
name <- getLine
let msg = "Hi, " ++ name ++ "!"
putStrLn msg
return name
... вы можете прочитать его, как императивную программу. Мы определяем процедуру с именем hello
. При выполнении сначала выполняется процедура для печати сообщения с вашим именем; затем он выполняет процедуру, которая читает строку ввода и присваивает результат name
; затем он присваивает выражение имени msg
; тогда это печатает сообщение; затем он возвращает имя пользователя как результат всего блока. Поскольку name
это a String
, это означает, что hello
это процедура, которая возвращает a String
, поэтому она имеет тип IO String
. И теперь вы можете выполнить эту процедуру в другом месте, так же, как она выполняетgetLine
.
Пффф, монады. Кому они нужны?