Monad: зависимые шаги и do-нотация
Вершина базовой иерархии. Аппликатив умел применять обёрнутую функцию, но каждый шаг был слеп к результату предыдущего. Монада снимает это: её
>>=пробрасывает результат одного шага в построение следующего. В этом уроке:>>=и его смысл, do-нотация как чистый сахар над>>=, три закона, Maybe/Either/список/функция как монады, помощникиwhen/unless/mapM_, и честный разговор о том, где монады заканчиваются.
Идея
Аппликатив упёрся в стену: в f <$> x <*> y шаг y не может зависеть от того, что вернул x. Структуры известны заранее, их только склеивают. А жизнь требует ветвления по результату: распарсил число, и в зависимости от него решаешь, что делать дальше.
Возьми такую задачу: достать из словаря значение по ключу, потом по этому значению достать второе. Каждый шаг это Maybe, и второй зависит от первого:
lookup2 :: Eq k => k -> k -> [(k, v)] -> [(v, w)] -> Maybe w
lookup2 k1 k2 m1 m2 =
case lookup k1 m1 of
Nothing -> Nothing
Just _ -> case lookup k2 m2 of
Nothing -> Nothing
Just w -> Just w
Лесенка из case, и она растёт с каждым шагом. Заметь паттерн: на Nothing бросаем всё, на Just x достаём x и идём дальше. Этот паттерн (продолжай только при успехе, пробрасывай неудачу) и есть Monad:
class Applicative m => Monad m where
(>>=) :: m a -> (a -> m b) -> m b
return :: a -> m a
return = pure
Applicative m => снова держит иерархию: монада это аппликатив, умеющий ещё одно. Главный метод это >>= (читается bind). Смотри на тип внимательно: слева структура m a, справа функция a -> m b, которая получает распакованное значение a и строит следующую структуру. Вот оно, отличие от <*>: функция справа видит результат и решает, что вернуть. return это синоним pure, исторический.
Инстанс Maybe это ровно наша лесенка, свёрнутая в одно правило:
instance Monad Maybe where
Nothing >>= _ = Nothing
Just x >>= f = f x
И lookup2 сжимается:
lookup2 k1 k2 m1 m2 =
lookup k1 m1 >>= \_ ->
lookup k2 m2 >>= \w ->
Just w
ghci> lookup 'a' [('a', 1), ('b', 2)] >>= \v -> lookup v [(1, "один"), (2, "два")]
Just "один"
ghci> lookup 'z' [('a', 1)] >>= \v -> lookup v [(1, "один")]
Nothing
Nothing в любом звене обрывает цепочку, и тебе не нужно проверять это руками: за тебя это делает инстанс. bind пробрасывает результат и эффект.
do-нотация это сахар
Цепочки >>= \x -> читаются тяжело, поэтому у них есть синтаксический сахар, do-нотация. Те же три строки:
lookup2 k1 k2 m1 m2 = do
_ <- lookup k1 m1
w <- lookup k2 m2
pure w
И это сахар, компилятор разворачивает do обратно в >>= по двум механическим правилам:
-- do { x <- e; rest } разворачивается в e >>= \x -> do { rest }
-- do { e; rest } разворачивается в e >> do { rest }
-- do { e } это просто e
x <- e это не присваивание, а распаковка через >>=: возьми значение из структуры e и назови его x. Стрелка <- живёт только внутри do. А >> это >>=, игнорирующий результат (когда x не нужен). Важно: do-нотация работает для любой монады, не только для IO. Та же запись соберёт и Maybe, и Either, и список.
Три закона
-- 1. левое тождество: return a >>= f == f a
-- 2. правое тождество: m >>= return == m
-- 3. ассоциативность: (m >>= f) >>= g == m >>= (\x -> f x >>= g)
Первые два говорят, что return (он же pure) ничего не добавляет: обернуть и сразу развернуть это нет-оп. Третий это ассоциативность из урока про моноиды, но для bind: группировка шагов не меняет результат, поэтому do-блок можно дробить на под-блоки. Законы держит любой честный инстанс; нарушишь, и do-нотация начнёт врать.
премиум-урок
Продолжение в премиуме: 1 200 ₽ в месяц или 10 000 ₽ в год. Вместе с ним серверные проверки задач и школьные токены AI-коуча.
Оплата пока не подключена: премиум и курсы открываются промокодом.