Давайте сначала проведем различие между изучением абстрактных понятий и изучением конкретных примеров из них.
Вы не будете слишком далеко игнорировать все конкретные примеры по той простой причине, что они совершенно вездесущи. Фактически, абстракции существуют в значительной степени потому, что они объединяют то, что вы будете делать в любом случае с конкретными примерами.
С другой стороны, сами абстракции, безусловно, полезны , но в них нет необходимости. Вы можете довольно далеко проигнорировать абстракции полностью и просто использовать различные типы напрямую. Вы захотите понять их в конце концов, но вы всегда можете вернуться к этому позже. На самом деле, я почти гарантирую, что если вы сделаете это, когда вернетесь к этому, вы будете бить себя по лбу и удивляться, почему вы потратили все это время на трудный путь, вместо того, чтобы использовать удобные инструменты общего назначения.
Взять хотя Maybe aбы пример. Это просто тип данных:
data Maybe a = Just a | Nothing
Это все, кроме самодокументирования; это необязательное значение. Либо у вас «просто» что-то типа a, либо у вас ничего нет. Допустим, у вас есть какая-то функция поиска, которая возвращает Maybe Stringпредставление для поиска Stringзначения, которое может отсутствовать. Таким образом, вы сопоставляете шаблон со значением, чтобы увидеть, какое оно:
case lookupFunc key of
Just val -> ...
Nothing -> ...
Это все!
На самом деле, вам больше ничего не нужно. Нет Functors или Monads или что-нибудь еще. Они выражают общие способы использования 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 .
Пффф, монады. Кому они нужны?