deniok: (Default)
Задача

Итак, наша задача - смонтировать императивное состояние чисто функциональными средствами. Состояние должно иметь тип (обозначим его через S). В дальнейшем, для простоты мы будем предполагать, что

type S = Int

однако будем писать S, чтобы не терять общности. Важно, что это конкретный, неполиморфный тип – мы же не хотим, чтобы в процессе вычисление состояние было то Int, то Char, то String?

Каков должен быть интерфейс для работы с состоянием? Мы должны уметь изменять это состояние (update) и считывать его значение (read). Ясно, что такое изменяемое состояние может жить только внутри монады; обзовём тип этого монадического контейнера SM, тогда:

readSM :: SM S

updateSM :: (S -> S) -> SM ()

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


Чуть-чуть про монадические вычисления

Напомним определение монады

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

Что они нам дают? Во-первых, они позволяют организовывать цепочку вычислений:

e1 >> e2 >> e3 >> e4

В засахаренном виде это выглядит так

do e1; e2; e3; e4

Более интересно, однако, то, что можно вытаскивать из выражения запакованное в монаду значение и использовать его где-то в дальнейших монадических вычислениях

e1 >>= \p -> e2 >> e3 >> e4

что под сахаром выглядит как

do p <- e1; e2; e3; e4

То есть внутри выражений e2, e3 и e4 мы можем обращаться к распакованному p.


State Monad

Для нашей монады SM это может выглядеть так

do x <- readSM
    updateSM (+1)
    y <- readSM
    return (x, y)

Теперь x будет связано с исходным состоянием, а y – с увеличенным на 1. Фактически это уже и есть ожидаемый “императивный” код. В дальнейшем монадическом вычислении мы можем использовать значения x и y как угодно (в примере мы возвращаем пару (x, y)).

Более того, мы можем написать код, который выглядит, как связывание переменной с мутирующим состоянием! Вот то, что так смутило [profile] geniepro:

do x <- readSM
    updateSM (+1)
    x <- readSM
    return x

Опля! “Пишем” в “переменную” x? Чистой воды иллюзия. Рассахарим это дело:

readSM >>= \x -> updateSM (+1) >> readSM >>= \x -> return x

Понятно, в чём фишка? Тут просто два разных икса, а конфликта мы избегаем благодаря разным областям видимости. В return, естественно, отправляется x из внутренней (вложенной) лямбды. Без сахара это, однако, выглядит полностью идентично мутирующей переменной.

Как устроена монада состояния внутри?

Конспективно это изложено в GIH (9.3), подробно – в книжке Душкина.


Технические замечания.

1. Говоря про монады, я для простоты изложения полностью игнорировал fail.


2. На самом деле конструкция

do p <-e1; e2; e3; e4

при трансляции рассахаривается не в лямбду

e1 >>= \p -> e2 >> e3 >> e4

а в let-выражение

let e p = do {e2 >> e3 >> e4}
in e1 >>= e

Это связано с тем, что в отличии от лямбды, где связывание мономорфизирует переменную, let-выражения полностью сохраняют полиморфизм. (см. параграф 3.14 Haskell98 Report, rev.2002)


Profile

deniok: (Default)
deniok

February 2022

S M T W T F S
  12345
6789101112
13141516171819
20212223 242526
2728     

Syndicate

RSS Atom

Most Popular Tags

Page Summary

Style Credit

Expand Cut Tags

No cut tags
Page generated Jun. 10th, 2025 10:48 am
Powered by Dreamwidth Studios