Monads

Монады  —  программируемые точки с запятой. Именно так. Монада предоставляет функции, позволяющие упорядочивать действия. Более того, между каждыми двумя действиями выполняется определённый фрагмент кода. Итак, монада  —  настраиваемая точка с запятой.

Сделаем шаг назад

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

Это интересно: монады происходят из области математики под названием “Теория категорий”. Для использования и понимания монад в Haskell не обязательно знать определения и теоремы этой математической дисциплины.

Секвенирование легче на других языках. Значит ли это, что Haskell плохо спроектирован? 

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

Представьте, что у вас есть функция с именем doCalculation, которая принимает число, выполняет некоторые вычисления и возвращает другое число. В Haskell выражение doCalculation(x) + doCalculation(x) можно заменить на 2*doCalculation(x). При этом мы можем быть уверены, что поведение программы не изменится. В Java эта замена вообще не выполнима, поскольку не гарантируется, что doCalculation возвращает один и тот же результат при каждом вызове. Например, метод может просто вызвать генератор случайных чисел и всегда возвращать другой результат. Кроме того, в таких языках, как Java, doCalculation может включать общие побочные эффекты: запись в базу данных или вывод на консоль.

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

Ближе к сути

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

— Тип Counter
data Counter a = Counter {
 counter :: Integer
 , element :: a
 } deriving (Eq, Show)

Тип Counter состоит из целого числа с именем counter и произвольного элемента. Для наших списков покупок элементом будет список строк. Счётчик будет представлять количество выполненных манипуляций. Давайте перейдём к самой важной части. Чтобы запрограммировать точку с запятой, включив секвенирование, мы пишем следующее:

-- Делаем Counter примером монады
instance Monad Counter where
 return x = Counter 0 x
 Counter n1 x >>= f = let Counter n2 y = f x
 in Counter (n1 + n2 + 1) y

Именно здесь происходит магия. Начнем с более простой функции.

return просто создаёт новый счётчик для данного элемента. Он может использоваться для построения нового счётчика, начинающегося с 0 манипуляций для данного списка покупок.

Как и всё в Haskell, монада  —  просто ещё одна функция. В нашем примере она имеет тип Counter a → (a → Counter b) → Counter b (для наших конкретных списков покупок a и b будут списками строк). А теперь то же самое  —  медленно: >>= получает счётчик с элементом (в коде он называется x). Он манипулирует элементом с помощью заданной функции, которая снова возвращает счётчик. Наконец, >>= возвращает новый счётчик, в котором целое, представляющее число манипуляций, является целым счётчика из данной функции плюс 1.

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

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

-- Добавляем элемент в список и создаём счётчик
add :: [a] -> a -> Counter [a]
add xs x = return (xs ++ [x])

— Удаляем элемент из списка и создаём счётчик
removeFirst :: [a] -> Counter [a]
removeFirst (x:xs) = return xs

-- Создаём пустой список
emptyShoppingList :: Counter [String]
emptyShoppingList = return []

Сейчас у нас есть все, чтобы создать типичный список покупок, по одному пункту за раз. Выполнение кода приводит к следующему результату:

Counter {counter = 5, element = ["Butter","Honey","Bagel"]}

Что здесь происходит?

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

Пустой список покупок по сути передаётся через строки и подвергается манипуляциям на каждом этапе. Ключевой момент: мы можем быть уверены, что “Bread” добавляется перед “Butter”. Как описано выше, так происходит благодаря определению программируемой точки с запятой.

Заметьте, что все в нашей операции с монадой состоит из функций. Каждая строка, кроме первой, является анонимной функцией, как и >>=.

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

-- Создание списка
createShoppingList :: Counter [String]
createShoppingList = do
 list <- emptyShoppingList
 list <- list `add` “Bread”
 list <- list `add` “Butter”
 list <- list `add` “Honey”
 list <- removeFirst list
 list `add` “Bagel”

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

Ещё одно важное замечание: после определения в программируемой точке с запятой, подсчёт полностью невидим внутри операции. Магия происходит внутри >>=. Поскольку функциональность монады можно свободно определить, это отличное место, чтобы скрыть что-то. Предопределённая монада maybe, например, скрывает проверку на null и пропускает все следующие строки, если видит null. Кроме того, монадой можно реализовать логирование и скрыть в >>=.

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

Читайте также:


Перевод статьи: Marcel Moosbrugger: Monads Are Just Fancy Semicolons