До сих пор каждый раз, когда мы реализовывали операцию >>=
, сразу же после извлечения результата из монадического значения мы применяли к нему функцию f
, чтобы получить новое монадическое значение. В случае с монадой Writer
после того, как это сделано и получено новое монадическое значение, нам по-прежнему нужно позаботиться о контексте, объединив прежнее и новое моноидные значения с помощью функции mappend
. Здесь мы выполняем вызов выражения f a
и получаем новое вычисление с состоянием g
. Теперь, когда у нас есть новое вычисление с состоянием и новое состояние (известное под именем newState
), мы просто применяем это вычисление с состоянием g
к newState
. Результатом является кортеж из окончательного результата и окончательного состояния!
Итак, при использовании операции >>=
мы как бы «склеиваем» друг с другом два вычисления, обладающих состоянием. Второе вычисление скрыто внутри функции, которая принимает результат предыдущего вычисления. Поскольку функции pop
и push
уже являются вычислениями с состоянием, легко обернуть их в обёртку State
:
import Control.Monad.State
pop :: State Stack Int
pop = state $ \(x:xs) –> (x, xs)
push :: Int –> State Stack ()
push a = state $ \xs –> ((), a:xs)
Обратите внимание, как мы задействовали функцию state
, чтобы обернуть функцию в конструктор newtype State
, не прибегая к использованию конструктора значения State
напрямую.
Функция pop
– уже вычисление с состоянием, а функция push
принимает значение типа Int
и возвращает вычисление с состоянием. Теперь мы можем переписать наш предыдущий пример проталкивания числа 3
в стек и выталкивания двух чисел подобным образом:
import Control.Monad.State
stackManip :: State Stack Int
stackManip = do
push 3
a <– pop
pop
Видите, как мы «склеили» проталкивание и два выталкивания в одно вычисление с состоянием? Разворачивая его из обёртки newtype
, мы получаем функцию, которой можем предоставить некое исходное состояние:
ghci> runState stackManip [5,8,2,1]
(5,[8,2,1])
Нам не требовалось привязывать второй вызов функции pop
к образцу a
, потому что мы вовсе не использовали этот образец. Значит, это можно было записать вот так:
stackManip :: State Stack Int
stackManip = do
push 3
pop
pop
Очень круто! Но что если мы хотим сделать что-нибудь посложнее? Скажем, вытолкнуть из стека одно число, и если это число равно 5
, просто протолкнуть его обратно в стек и остановиться. Но если число 5
, вместо этого протолкнуть обратно 3
и 8
. Вот он код:
stackStuff :: State Stack ()
stackStuff = do
a <– pop
if a == 5
then push 5
else do
push 3
push 8
Довольно простое решение. Давайте выполним этот код с исходным стеком:
ghci> runState stackStuff [9,0,2,1,0] ((),[8,3,0,2,1,0])
Вспомните, что выражения do
возвращают в результате монадические значения, и при использовании монады State
одно выражение do
является также функцией с состоянием. Поскольку функции stackManip
и stackStuff
являются обычными вычислениями с состоянием, мы можем «склеивать» их вместе, чтобы производить дальнейшие вычисления с состоянием:
moreStack :: State Stack ()
moreStack = do
a <– stackManip
if a == 100
then stackStuff
else return ()
Если результат функции stackManip
при использовании текущего стека равен 100
, мы вызываем функцию stackStuff
; в противном случае ничего не делаем. Вызов return
()
просто сохраняет состояние как есть и ничего не делает.
Получение и установка состояния
Модуль Control.Monad.State
определяет класс типов под названием MonadState
, в котором присутствуют две весьма полезные функции: get
и put
. Для монады State
функция get
реализована вот так:
get = state $ \s –> (s, s)
Она просто берёт текущее состояние и представляет его в качестве результата.
Функция put
принимает некоторое состояние и создаёт функцию с состоянием, которая заменяет им текущее состояние:
put newState = state $ \s –> ((), newState)
Поэтому, используя их, мы можем посмотреть, чему равен текущий стек, либо полностью заменить его другим стеком – например, так:
stackyStack :: State Stack ()
stackyStack = do
stackNow <– get
if stackNow == [1,2,3]
then put [8,3,1]
else put [9,2,1]