Еще Одно Руководство по Монадам (часть 2: функции >>= и return)

http://mvanier.livejournal.com/4305.html
  • Перевод
By Mike Vanier

Две фундаментальные монадические операции


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

К этому моменту, я надеюсь, у вас сложилось хотя бы смутное ощущение монад, что они такое и для чего используются. Я уже упоминал одну из особенностей функционального программирования — композицию функций, благодаря которой мы создаем новые функции, объединяя старые. Функциональные программисты постоянно говорят о «комбинируемости» {1}, подразумевая, что если что-то в языке программирования не комбинируется, значит, оно немногого стоит. Аналогично, наши новоявленные монадические функции не были столь же полезны, если бы они не компоновались так, как это есть на самом деле. Но мы еще увидим, для их композиции нельзя использовать стандартную функцию «точка» (.) языка Haskell. Мы придем к выводу, что тут нужно что-то большее, и определим две фундаментальные монадические операции (или, для начала, их типы).

Пусть у нас есть такие монадические функции:

:: a -> m b
:: b -> m c


для некоторой монады m. Если вы хотите более конкретный пример, можете представить себе, что f и g — это функции в монаде IO:

:: a -> IO b
:: b -> IO c


Однако то же самое справедливо и для других монад. Вспомним (для случая с IO), что функция f принимает значение типа a и выводит значение типа b, а в процессе, может быть, взаимодействует с вводом/выводом (IO). Функция g, в свою очередь, берет значение типа b и выводит значение типа c, а в процессе, может быть, работает с вводом/выводом. С помощью композиции этих функций мы надеемся получить функцию h:

:: a -> IO c


то есть, результат — это функция, которая берет значение типа a, выводит значение типа c и в процессе работает с вводом/выводом (где ввод/вывод — это комбинация того, что делают f и g). На псевдоязыке мы могли бы записать это так:

соединить функцию:
    (:: a -> IO b) 
с функцией:
    (:: b -> IO c) 
чтобы получить:
    (:: a -> IO c)


Однако в Haskell оператор композиции (точка) ничего не знает про тип IO, поэтому не станет работать с нашими функциями f и g. Для сравнения посмотрим на чистые функции с обычными типами p, q, r без всяких IO:

:: a -> b
:: b -> c
:: a -> c


Операторы (.) и (>.>) тут подходят:

(.) :: (-> c) -> (-> b) -> (-> c)
= q . p
(>.>) :: (-> b) -> (-> c) -> (-> c)
= p >.> q


Но ни один из них не будет работать с монадическими функциями:

:: a -> IO b
:: b -> IO c
:: a -> IO c
. f     --> ошибка типизации! Несоответствие между IO b и b
>.> g   --> ошибка типизации! Несоответствие между IO b и b


Нельзя использовать монадическое значение типа IO b, если требуется тип b. (Это наиболее частая ошибка монадических программ на Haskell.) Нам нужна специальная функция монадической композиции, которую я зову mcompose (от «monadic compose»). У нее следующий тип:

mcompose :: (-> m b) -> (-> m c) -> (-> m c)


Она работает для любой монады, в том числе для монады IO. Подставляя IO, мы получим соответствующее определение функции:

mcompose :: (-> IO b) -> (-> IO c) -> (-> IO c)


Мы могли бы ее использовать для композиции монадических функций f и g. Тип функции h был бы корректным:

:: a -> IO b
:: b -> IO c
:: a -> IO c
= f `mcompose` g


(Здесь функция mcompose окружена обратными апострофами. Это элегантный синтаксический сахар, который позволяет сделать из двухаргументной функции инфиксный оператор. Не забывайте, что операторы в Haskell — это просто функции, помещенные между своими операндами.) Каким-то мистическим образом (пока еще мистическим), функция mcompose (или оператор, если хотите) способна:

  1. принять исходное значение типа a;
  2. применить функцию f к нему (обычное применение функций) и получить результат типа IO b;
  3. взять значение типа IO b от функции f и извлечь значение типа b (это то, что у нас не получается);
  4. взять значение типа b и применить функцию g к нему (опять обычное применение функций), чтобы получить значение типа IO c, которое и есть искомый результат.


Единственное, что мы не можем еще сделать, это шаг (3), — получить значение типа b из значения типа IO b. Давайте придумаем функцию extract, которую бы мы могли использовать для извлечения. Вот ее тип:

extract :: IO b -> b


А если обобщить на все монады, получим:

extract :: m b -> b


Оказывается, что если бы такая функция существовала, она бы нивелировала все преимущества монад и чистого функционального программирования! Есть причина, по которой нам нужны монады. Мы хотим хранить специальные понятия вычислений (монадические функции) отдельно от чистых функций, потому что иначе не было бы никаких гарантий, что чистые функции — чистые. Это важный момент, и я собираюсь потратить на него немного времени, а потом мы вернемся к монадической композиции.

*Заметка на полях.* На самом деле, для некоторых монад есть эквивалент функции extract, что не влечет за собой никаких проблем. Однако я должен сказать, что обобщенная на все монады функция extract запрещена.


Нам бы хотелось точно знать, что функции без монадических типов являются чистыми. Хотя вообще-то в Haskell монадические функции являются чистыми, потому что они сделаны как чистые функции, возвращающие монадическое значение. Но мы хотим гарантировать, что немонадические (чистые) функции даже пытаться не будут работать с монадическими значениями. Тогда они точно будут чистыми. Например, чистая функция hh типа

hh :: a -> c


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

hh :: a -> IO c


Подобные гарантии, подкрепленные системой типов, — это одна из главных сильных сторон Haskell. Они позволяют нам по одному взгляду на определение функции быть на 100% уверенными, что она не взаимодействует с вводом/выводом (например).

Однако, если бы у нас была функция extract, мы могли бы составить hh — якобы чистую функцию из нечистых, оперирующих I/O:

ff :: a -> IO b
gg :: b -> c
hh = ff >.> extract >.> gg  -- или то же самое: hh = gg . extract . ff


Даже если никогда не предполагалось, что функция hh будет делать ввод/вывод, вы могли бы ее создать с помощью extract и обычного оператора композиции, а тип hh был бы чистым, — но внутри все равно выполнялись бы операции ввода/вывода. И не получилось бы отделения IO (а так же других монадических вычислений) от чистых вычислений. (А ведь это одна из главных причин введения монад.) Заметим, кстати, что в точности эта ситуация и творится с большинством обычных языков программирования, из-за чего их системы типов не гарантируют, что функции чистые. В отличие от Haskell, — нам нравится его механизм чистых функций, его система типов, которая обязывает чистые функции быть чистыми, и потому-то в Haskell нет функции extract.

Есть одна небольшая проблемка с тем, что я сейчас сказал: вообще-то это ложь. Существует функция unsafePerformIO с типом IO a -> a, то есть, это версия extract для монады IO. Слово «unsafe» («небезопасный») намекает на то, что вы должны избегать этой функции, если не знаете, что в точности хотите сделать, или не готовы к странным сбоям. Мне никогда не приходилось использовать unsafePerformIO, но легальные случаи есть, например, глубоко в реализации компиляторов Haskell. Просто забудьте, что я вам это сказал, хорошо? Мне неловко из-за этого. Извините. {3}

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

Ну, прежде всего, попробуем придумать что-нибудь попроще, чем mcompose. Скажем, некую функцию mapply (монадическое применение), у которой следующий тип:

mapply :: m b -> (-> m c) -> m c


А если спуститься от общих монад к IO, то получим:

mapply :: IO b -> (-> IO c) -> IO c


Она названа mapply из-за своей схожести с обычным оператором применением функций. Вспомним, например, оператор >$>, ранее определенный подобным образом:

(>$>) :: b -> (-> c) -> c


То же самое, что и mapply, только нет никаких «m» (типы не монадические). mcompose тривиально выражается через mapply:

mcompose :: (-> m b) -> (-> m c) -> (-> m c)
mcompose f g x = (f x) `mapply` g  -- или: mapply (f x) g


Так как стрелка (->) в определении типов правоассоциативная, мы можем опустить скобки у последнего элемента:

mcompose :: (-> m b) -> (-> m c) -> a -> m c
mcompose f g x = (f x) `mapply` g


Может быть, эту версию mcompose понять проще, чем предыдущую, но они — одно и то же. Стоит отметить, что x имеет тип a, а тип результата — m c. Вот что мы делаем здесь: мы применяем функцию f к x, чтобы получить значение типа m b; потом передаем в mapply это значение (типа m b) и функцию g, получая таким образом интересующее нас значение типа m c. Получается, что нам ненужно, чтобы где-то была функция mcompose; если у нас уже есть mapply, через нее мы и сами можем написать mcompose. И, на самом деле, mapply — одна из двух фундаментальных монадических операций. Она обычно называется «bind» («связать») и записывается как символьный инфиксный оператор >>=:

(>>=) :: m a -> (-> m b) -> m b


Стоит заметить, что я слегка изменил определение типа: заменил b на a и c на b. Но это и не важно, поскольку a, b, c — переменные типов, они работают с любыми типами.

Я бы хотел подчеркнуть, что оператор >>= в высшей степени абстрактен. Его первый аргумент — это значение типа m a, где a может быть вообще любым типом, а m — любым монадическим конструктором типа. Вторым аргументом следует функция a -> m b, где a и b — это тоже любые типы, и m — это, опять же, любой монадический конструктор типа. Наконец, у возвращаемого значения тип m b, где b может быть любым типом и m может быть любым монадическим конструктором типа. (Для бывалых программистов на Haskell подобные определения типов становятся второй натурой, но для новичков это может быть сложным.) Можете специализировать определение до монады IO, и получите монадический оператор применения на монаде IO:

(>>=) :: IO a -> (-> IO b) -> IO b


Мы скоро увидим, что система типов Haskell позволяет использовать обобщенный оператор >>= для самых разных монад (это круто, да?).

Предположим, что у нас есть оператор >>=, с ним мы можем соединить f и g, чтобы получить h:

-- предположим, нам дано:
:: a -> m b
:: b -> m c
 
-- определение h:
:: a -> m c
h x = f x >>= g


Мы также можем переписать h по-другому:

= \x -> f x >>= g


где \x -> ... — это, как было сказано ранее, обозначение анонимных функций в Haskell {4} (в этом случае с одним аргументом x); обе версии функции h значат одно и то же. Используя mcompose, мы можем записать такое уравнение:

= f `mcompose` g = mcompose f g = \x -> (f x >>= g)


То есть, наша mcompose определяется как

mcompose f g = \x -> (f x >>= g)


На самом деле в Haskell уже есть стандартный монадический оператор композиции >=>:

>=> g = \x -> (f x >>= g)  -- то же самое, что и (f `mcompose` g) но более краткое.


Предположив, что у нас есть монадический оператор применения >>=, мы смогли легко задать монадический оператор композиции >=>. Значит, монадический оператор применения (bind-оператор) концептуально важен. Далее мы увидим, что для каждой монады определен свой специфический оператор >>=, отличающийся от всех остальных; эту задачу очень хорошо решают классы типов Haskell. Кстати, в компиляторе GHC оператор >=> определен в модуле Control.Monad.

Теперь давайте вспомним, что обычный оператор применения мы записывали двумя способами:

($) :: (-> b) -> a -> b


и

(>$>) :: a -> (-> b) -> b


Какой из этих операторов применять, зависит от того, в каком порядке мы хотели передавать аргументы. (Замечательно определить оба оператора, чтобы ими пользоваться тогда, когда это удобно.) Монадический оператор применения мы тоже можем записать двумя способами. Первый способ — это bind-оператор >>= с типом

(>>=) :: m a -> (-> m b) -> m b


который является аналогом обычного оператора применения >$>. Тривиально задается монадический оператор применения, принимающий аргументы в обратом порядке:

(=<<) :: (-> m b) -> m a -> m b
=<< x  =  x >>= f


Еще вы можете взять функцию flip, которая берет функцию двух аргументов и возвращает ту же функцию, но с обратным порядком аргументов:

flip :: (-> b -> c) -> (-> a -> c)
flip f = \x y -> f y x


Оператор =<< через нее определяется так:

(=<<) = flip (>>=)


Вы получите дополнительные очки функциональной крутизны {5}, если ваши определения будут такими краткими, как это.

И опять: мы можем задать монадический оператор композиции для обратного порядка операндов:

(>=>) :: (-> m b) -> (-> m c) -> (-> m c)  -- уже есть
 
(<=<) :: (-> m c) -> (-> m b) -> (-> m c)
(<=<) = flip (>=>)


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

Если вы поняли все это, мои поздравления! Все самое трудное позади. Надеюсь. {6}

Есть еще одна фундаментальная монадическая операция, о которой я собираюсь рассказать. Для затравки рассмотрим следующий сценарий. Пусть вам потребовалось соединить монадическую функцию с немонадической. Иными словами, у вас есть такие функции:

:: a -> m b   -- монадическая
:: b -> c     -- немонадическая


Проблема в следующем. Вы не можете использовать обычную функцию композиции для f и g, потому что m b — не то же самое, что b. И монадическая композиция тоже не подходит, — она ничего не знает о типе b -> c, ей нужен монадический тип b -> m c. Что вы будете делать?

Если бы у вас была функция extract, разобранная ранее, вы бы могли соединить две функции таким вот образом:

:: a -> c
= f >.> extract >.> g


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

:: a -> m c
= f [как-то комбинируем с] g


Ну, мы знаем, что монадическая композиция не годится, потому что у g неправильный тип (который должен быть b -> m c). Однако она пригодилась бы, сумей мы преобразовать обычную функцию в монадическую. Пусть функция, выполняющая такое преобразование, называется functionToMonadicFunction.

functionToMonadicFunction :: (-> c) -> (-> m c)


Функцию h теперь можно записать иначе:

:: a -> m c
= f >=> (functionToMonadicFunction g)


Оказывается, все что нужно — это определить функцию functionToMonadicFunction, а это очень просто, потому что уже существует монадическая функция с именем (возможно, сбивающим с толку) return. У нее следующий тип:

return :: a -> m a


где a — любой тип, и m — любой монадический конструктор типа. Функция return конвертирует обычное значение в соответствующее монадическое значение для любой монады m, — и это все, что она делает. Вот мы ее сейчас и задействуем.

Если у вас есть return, то functionToMonadicFunction выражается через нее просто:

functionToMonadicFunction :: (-> b) -> (-> m b)
functionToMonadicFunction f = \x -> return (f x)


Или, если пожелаете быть крутыми, можете воспользоваться композицией функций:

functionToMonadicFunction :: (-> b) -> (-> m b)
functionToMonadicFunction f = return . f


Или даже:

functionToMonadicFunction :: (-> b) -> (-> m b)
functionToMonadicFunction = (return .)


В последнем примере задействована замечательная возможность Haskell под названием «сечения». Все три варианта эквивалентны.

Заметьте, что я снова заменил буквы типов: b на a и c на b; какая буква будет в типе — не имеет значения. Главная мысль сказанного в том, что с помощью return мы можем соединять монадические и немонадические функции, получая снова монадические. return — вторая фундаментальная монадическая операция.

*Заметка на полях.* Если вы программируете в основном в императивном стиле, слово return может показаться вам несколько раздражительным. Просто запомните, что это не ключевое слово в Haskell, и тут ничего не возвращается из функции. Постарайтесь не думать о return как о return в императивном языке программирования.


Название «return» на самом деле пришло из понимания монадических значений как «действий». В этом смысле функция return берет простое значение и производит монадическое значение. Этим монадическим значением уже «что-то делается», а результатом будет оригинальное значение. Стоит заметить также, что фактически return — монадическая функция. Сложив эти две мысли, вы можете сделать вывод (или хотя бы догадаться), что return — монадическая версия единичной (identity) функции (эта функция отображает значение в само себя, то есть \x -> x). Мы к этому вернемся, когда будем говорить о монадных законах.

Давайте возьмем return в оборот, соединив нашу монадическую функцию f с немонадической функцией g, чтобы получить монадическую h.

= f >=> (return . g)


И это правильная запись, поскольку return. g преобразует g в монадическую функцию.

После всего сказанного вы, наверное, задаетесь вопросом, сколько еще монадических операций нам придется перепахать, прежде чем мы определим их все. Как обычно говаривал профессор Фарнсворт: «Хорошие новости!» Их всего две! Мы для удобства задали еще несколько не столь важных операций, но из них только >>= и return обязательно должны быть.

Есть еще один довольно своеобразный момент с return. Сказано, что тип return выглядит как a -> m a. Когда мы говорим, например, return 10, какой будет тип этого выражения? Он может быть IO Int, Maybe Int или еще какой-нибудь монадический Int. Откуда нам знать, что за монада тут стоит? Ведь монадическое значение IO Int — совсем не то же самое, что и Maybe Int; так что мы не просто интересуемся о верном типе, мы хотим понимать, что это за значение такое — return 10!

В Haskell смысл return 10 определяется контекстом. Валидатор типов (type checker) должен убедиться, что функции получают аргументы с нужными типами, так что если return 10 передан в функцию, где ожидается значение IO Int, то return 10 будет трактоваться как IO Int. (Это же правило справедливо и для других монад.) Иначе говоря, значение выражения return 10 зависит от типа, в контексте которого оно используется. Если пожелаете, вы можете явно задать тип выражения return 10 с помощью, например, записи (return 10 :: IO Int), но это редко бывает нужно.

Подведем итог сказанного.

  • Существуют две фундаментальные монадические операции: «bind» (оператор >>=) и return.
  • Bind-оператор — это монадический оператор применения. Через него можно задать монадический оператор композиции, который выглядит так: >=>.
  • Оператор return преобразует обычные значения в монадические. С его помощью определяется функция преобразования обычных функций в монадические.


А что же на самом деле значат монадическое применение и монадическая композиция?


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

Итак, мы сказали, что нельзя задать функцию extract, которая из монадического значения делала бы обычное. Однако, если нам требуется комбинировать две монадические функции в третью, каким-то образом все равно придется извлечь обычные значения из монадических.

(>=>) :: (-> m b) -> (-> m c) -> (-> m c)
>=> g = {- что-нибудь -}


Здесь показано: функция f берет значение типа a и возвращает монадическое значение типа m b; функция g берет значение типа b и возвращает значение типа m c. И это, конечно, выглядит так, как будто где-то внутри обычное значение «распаковывается» из монадического. Еще мы обсудили, что можем определить монадическую композицию >=> в терминах монадического применения (>>=). Взглянем снова на >>=:

(>>=) :: m b -> (-> m c) -> m c
mv >>= g


где mv является некоторым монадическим значением типа m b. И ведь тоже — без всякой extract, — как так получается значение типа b из значения типа m b, чтобы его передать в функцию g?

Ответ для всех монад разный. У каждой монады свой способ «распаковать» монадическое значение, чтобы передать его как обычное в другую монадическую функцию. Иначе говоря, оператор >>= специфичен для всех монад, и в его реализации скрыто, как монадическое значение распаковывается и передается дальше. Также у каждой монады свой оператор return.

С этого момента я буду использовать несколько иную терминологию в отношении произвольных монад. Оператор >>= принимает входное монадическое значение (также известное как «действие»), «распаковывает» его в обычное (немонадическое) значение (распаковка для всех монад выглядит по-разному), и затем передает это обычное значение в монадическую функцию. В ней уже производится монадическое значение («действие») и возвращается финальный результат.

Теперь, когда мы все это изучили, давайте поговорим о классе типов "Monad". Позже мы рассмотрим определение оператора >>= для некоторых монад, и вы узнаете, как там делается распаковка.

Класс типов Monad


>>=это и есть монадический оператор применения, а returnэто и есть функция преобразования обычного значения в любое монадическое. Так я говорил выше. Однако эта терминология неаккуратна, поскольку было также сказано, что у каждой монады должны быть определены собственные версии этих операторов/функций, отличающиеся от других. С другой стороны совсем нехорошо по-разному называть операторы >>= и return. Мы получили бы неприятные вещи вроде этой:

IO>>= :: IO a -> (-> IO b) -> IO b
IOreturn :: a -> IO a
 
Maybe>>= :: Maybe a -> (-> Maybe b) -> Maybe b
Maybereturn :: a -> Maybe a


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

Проблема красиво решается «классами типов» языка Haskell. (Помните, я говорил, что руководство будет проще, если вы уже знакомы с классами типов? Классы типов не имеют ничего общего с классами из ООП, это абсолютно разные вещи. Классы типов Haskell больше похожи на интерфейсы времени компиляции). Класс типов — это способ сказать, что для кучи разных типов есть разные воплощения одноименных функций (или операторов). Так, например, в классе типов Eq содержится оператор == с типом a -> a -> Bool (где все a — один и тот же тип). Чтобы тип a относился к классу типов Eq, для него должен существовать подходящий оператор == (сравнение на равенство). Числовые типы Int и Float принадлежат классу типов Eq (еще можно сказать — они являются экземплярами класса Eq), значит, у каждого из них есть свой осмысленный оператор ==. На Haskell это записывается следующим образом:

class Eq a where
  (==) :: a -> a -> Bool
 
instance Eq Int where
  (==) = intEquals     -- тип intEquals такой: (Int -> Int -> Bool)
 
instance Eq Float where
  (==) = floatEquals   -- тип floatEquals такой: (Float -> Float -> Bool)


Считается, что intEquals — это функция сравнения для Int, а floatEquals — для Float. (Я еще не сказал про оператор неравенства; там такая же идея.) Вот и все. Теперь мы можем использовать оператор == для сравнения целых и вещественных чисел между собой. Или для сравнения любых объектов, лишь бы был соответствующий экземпляр класса типов Eq. И это очень удобно. Между тем, заметьте, что == требует, чтобы у сравниваемых элементов был один и тот же тип; нельзя сравнить, скажем, Int и Float.

Идем далее. Повторю, что уже говорил: все монады в Haskell являются конструкторами типов, и мы показали, что все монады предоставляют независимую реализацию оператора >>= и функции return. Из этих двух тезисов можно сделать вывод, что существует класс типов под названием (вы правильно догадались) Monad, который изначально определен так:

class Monad m where
  (>>=)  :: m a -> (-> m b) -> m b
  return :: a -> m a


Класс типов Monad не сильно сложнее, чем Eq. Нужно определить всего две функции/оператора, — не такое уж и большое дело, потому что мы уже это знаем. Типы двух функций/операторов те же, которые мы обсуждали выше.

Странность с классом типов Monad в том, что он не таков, как Eq. Monad является «конструктором класса», для которого экземпляры (обозначенные как m) не типы, но конструкторы типов; мы как раз уже убедились, что все монады должны быть конструкторами типов. Вот так мы задаем экземпляр конструктора класса (для примера взята монада Maybe):

instance Monad Maybe where
  (>>=)  = {- версия (>>=) для Maybe -}
  return = {- версия return для Maybe-}


Для обычных классов и для конструкторов классов выбрана одна и та же запись, что может вас немного запутать, но немного практики, — и все встанет на свои места. Возможно, было бы лучше, если бы Haskell использовал такую нотацию:

constructorClass Monad m where
  (>>=) :: m a -> (-> m b) -> m b
  return :: a -> m a
 
constructorInstance Monad Maybe where
  (>>=)  = {- the Maybe version of (>>=) -}
  return = {- the Maybe version of return -}


Но это слишком подробно. Обычно хватает контекста, чтобы понять, что происходит.

А теперь давайте разберем простой пример, и потом уже поговорим о классе типов Monad.

Пример


Одна из самых простых программ с монадой IO читает текст с терминала и печатает его назад (с новой строкой в конце). Естественным образом для этого используются getLine и putStrLn. Вспомним их типы:

getLine  :: IO String
putStrLn :: String -> IO ()


Для них нельзя использовать монадический оператор композиции >=>, потому что getLine имеет вид монадического значения, а не монадической функции. Но мы можем взять оператор >>=:

readAndPrintLine = getLine >>= putStrLn


Мы здесь выполняем (монадическое) применение монадической функции putStrLn к монадическому значению getLine. В терминах «действий», о которых мы говорили ранее, мы можем думать так: getLine — это «действие», которое читает строку текста с терминала и «возвращает» ее монадическое значение; оператор >>= «распаковывает» введенную строку из монадического значения, передавая ее в putStrLn; putStrLn в свою очередь печатает ее в терминал и ничего не возвращает (вообще-то возвращает пустое () как монадическое значение).

Мы могли бы написать это явно:

readAndPrintLine = getLine >>= (\s -> putStrLn s)


Обратите внимение, что запись (\s -> putStrLn s) в точности то же самое, что и просто функция putStrLn, по аналогии с записью (\x -> cos x), которая ничто иное как просто cos. Так что мы ничего важного здесь не изменили. Но становится более понятным, как что-то (строка текста), будучи возвращенной из getLine, передается в putStrLn и печатается в терминале.

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

Содержание

Часть 1: основы
Часть 2: функции >>= и return
Часть 3: Монадные Законы
Часть 4: Монада Maybe и монада списка

Примечания

{1} В оригинале — «composability».
{3} В оригинале — «Excuse me while I go wash my hands.» — практически непереводимое устойчивое выражение, выражающее конфузию автора, его отстранение от сказанного и самообвинение. Здесь, думаю, была бы неуместна близкая по словам русская фраза «Я умываю руки.» В начале следующего абзаца стоит предложение «OK, I'm back.», — имеется в виду, что автор вернулся после мытья рук. Заменено для поддержания смысла.
{4} Еще одно название анонимнх функций: лямбда-функции.
{5} В оригинале — «extra points for functional coolness»
{6} В оригинале — устойчивое выражение «It's all downhill from here». У него иронический смысл, сочетающий в себе две противоположности: 1. Забрались на высоту, спуск будет проще; 2. Дальше будет только хуже.
  • +15
  • 5,8k
  • 4
Поделиться публикацией
Похожие публикации
AdBlock похитил этот баннер, но баннеры не зубы — отрастут

Подробнее
Реклама
Комментарии 4
  • +1
    Гениальная статья! Все понял и чувствую себя почти что просвещенным :) Большое спасибо за перевод.

    Скажите, а верна ли моя догадка, что IO на самом деле является внутренней монадой компилятора (подобно встроенным типам, таким, как Int), и когда main возвращает эту монаду, как бы «за кадром» начинается выполнение программы? То есть ни в каком Prelude не объявлен соответствующий экземпляр класса Monad.
    • 0
      У меня есть подозрение, что Prelude вообще лишь «агрегатор» многих функций из многих модулей. Что касается монады IO, то в литературе, которая мне встречалась, говорилось, что IO ничем от других монад не отличается, за исключением того что его конструктор (собственно, «IO») не экспортируется из модуля System.IO, — и таким нехитрым образом (через цепочку следствий) нельзя написать функцию с чистым типом, у которой внутри вызывались бы функции из монады IO.

      Однако, ручаться не буду, потому что в реальности оно обычно не так, как на самом деле. :) Для уточнения этого вопроса я, пожалуй, загляну накануне в исходники какого-нибудь компилятора (ну, GHC, скорее всего). Точно знаю: «зашитые» в язык вещи есть. Например, конструктор списков (квадратные скобки — []). Ничто не мешает и монаде IO быть каким-то образом «зашитой» там же.
    • 0
      У каждой монады свой способ «распаковать» монадическое значение

      Оператор >>= принимает… «распаковывает» его в обычное (немонадическое) значение (распаковка для всех монад выглядит по-разному), и затем передает это обычное значение в монадическую функцию. В ней уже производится монадическое значение («действие») и возвращается финальный результат.

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

      Аналогично с IO — распаковка в смысле extract должна была бы осуществлять действия, связанные с вводом-выводом. Но мы-то знаем, что при >>= никаких действий ввода-вывода не осуществляется.

      Если Вы имели в виду какой-то другой смысл слова «распаковка», то стоит это пояснить.

      Я бы использовал другое объяснение — >>= производит применения функции к значению внутри монады, не производя распаковки, а затем получающееся «дважды монадическое» значение некоторым специфичным для монады образом (известным, как функция join) перепаковывает в просто монадическое.

      На примере списка — монадическая функция применяется к каждому элементу списка, затем получившийся список списков делается плоским.
      • 0
        Я буду рад перевести ваше сообщение и отправить его автору. :)

        Вообще-то, конечно, тонкости здесь есть. Мы можем собрать последовательность действий при помощи bind или do-нотации и сохранить их в соответствующей структуре до будущего выполнения. В этом случае, действительно, никакой «распаковки» не происходит. Но когда дойдет до выполнения, оператор >>= в монаде IO все-таки занимается «распаковкой». Он берет значение типа IO a, достает из него значение a (все так же находясь в монаде IO) и передает его в следующую функцию. Не стоит путать «выполнение» и «распаковку»: при ленивом порядке вычислений выполнение может начаться не в том же участке кода, где оно написано. То есть, «выполнение» — это реальные действия, которые печатают что-то в консоли, берут из консоли, запускают генератор случайных чисел и так далее, а «распаковка» — это указание, что когда будем выполнять эти инструкции, то должны извлечь вот эти данные. «Распаковка» декларирует действие для будущего выполнения, но не запускает его тут же, немедленно.

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