Композиция

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

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

Раньше у нас был один единственный вычислительный блок, а теперь – несколько маленьких блоков. Следовательно, должен быть некий абстрактный способ соединить наши маленькие блоки воедино и снова получить блок. Таким образом можно “склеить” не только два блока в один большой, но и соединить любое их количество, поскольку склеивание двух блоков снова даёт блок, а мы уже умеем соединять блоки. Перефразируя в теории типов:

glueOneToAnother :: Block -> Block -> Block

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

reversedComposition :: (a -> b) -> (b -> c) -> (a -> c)

Типа a, b и c абстрактны. Заметим, что в общем случае можно соединять только взаимно совместимые вычислительные блоки, ведь каждый из них обладает определённым входом и выходом. В примере выше мы получили блок (a -> c) (что бы это ни значило) из двух блоков; последовательность скливания при этом важна, иначе ничего не получится. Не получится склеить блоки (a -> b) и (c -> d) в один.

Композиция сущностей изучается в теории категорий.

Монада

В примере reversedComposition выше каждый блок полностью независим и, помимо входов и выходов, не имеет связи с остальными блоками. Мы говорим, что у блоков нет общего вычислительного контекста. Они ничего не разделяют (share) друг с другом.

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

glueOneToAnotherBad :: Ctx a -> Ctx b -> Ctx b

Склейка блоков даёт блок, блоки должны быть в некотором смысле совместимы. В сигнатуре выше опущена маленькая деталь: наш аппарат, т.е. цепочку блоков когда-нибудь запустят (настроив начальное окружение Ctx), и каждый блок вернёт из вычислительного контекста “чистый” результат, типа a, b и т.д. Собственно, аппарат и построен по такому принципу: рано или поздно чистый входной сигнал a преобразуется в чистый выходной сигнал b.

Значит, для склеивания двух блоков на втором месте аргумента glueOneToAnotherBad нам нужен не просто блок Ctx b, а конструктор вычислительных блоков, который, фактически, каждый раз будет строить новый блок для обработки конкретного чистого значения (т.е. результата запуска предыдущего блока). Мы говорим, что блок Ctx a – это чистое значение a, “завёрнутое” в вычислительный контекст Ctx. Вот как выглядит конструктор блока Ctx b, который получает на вход чистый сигнал a:

makeCtxBFromA :: a -> Ctx b

Перепишем нашу функцию склейки (композиции) совместимых блоков в блок:

glueOneToAnotherGood :: Ctx a -> (a -> Ctx b) -> Ctx b

Это и есть >>=, операция монадического связывания!

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

Теперь понятно, как это работает. return конструирует блок, который, после запуска, просто возвращает данное чистое значение и больше ничего не длает:

> getLine >>= \str -> return (length str)

Тридцать секунд прошли, спасибо за внимание! :)