好耶!是 State Monad!

HoshinoTented

2019-10-31 22:14:33

Personal

State Monad 维护了一个状态,可以用来模拟变量,这在纯函数式的编程当中是非常便利的。 ## Stack? 假如要实现一个栈,需要怎么做呢? ```haskell push :: a -> [a] -> [a] push = (:) pop :: [a] -> (a, [a]) pop [] = error "qaq" pop (x:xs) = (x, xs) ``` 而在使用的时候…… ```haskell uncomfortableStack :: [Int] -> [Int] uncomfortableStack xs = let xs' = push xs 1 (x, xs'') = pop xs' xs''' = push $ x + 1 (x', xs'''') = pop xs''' xs''''' = push $ x' + 2 in xs''''' ``` 会发现花费了许多代码在维护 栈 上,接下来要介绍的 State Monad,就可以十分优雅地维护一个栈了。 ## Let me see... State Monad 用来维护一个 **状态(State)**。在编程中常常需要维护一个状态,比如一个变量。 接下来看看 State 的定义(在 mtl 库中不是这么定义的,接下来会解释其原因): ```haskell newtype State s a = State { runState :: s -> (a, s) } deriving (Functor) ``` State 包含了一个函数,接收一个状态(s),进行运算后返回一个结果(a)和一个新状态。 既然 State 是一个 Monad,那就再看看 State Monad 的实现。 ```haskell instance Monad (State s) where return a = State $ \s -> (a, s) (State m) >>= f = State $ \s -> let (v, s') = m s (State g) = f v in g s' instance Applicative (State s) where pure = return (<*>) = ap ``` `return` 函数仅仅将一个值包装进 State Monad,而不对状态做任何修改,所以只需要 `\s -> (a, s)`。 而 `(>>=)` 需要把 State 中的 结果 取出来,所以需要有一个 来自未来 的状态 `s` 传入到状态中的 runState 拿到结果和一个新的状态。 接着用 f 应用到 得到的结果 上,就又得到了一个新的 State。 最后把得到的新 State 中的 runState 应用在得到的新状态上,得到属于 来自未来 的状态计算的结果。 ## State! 回到一开始介绍的栈问题,要怎么用 State 来实现一个栈? ```haskell push :: a -> State [a] () push i = State $ \xs -> ((), i:xs) ``` `push` 动作不会返回任何结果,所以是 `()` 值,而它会修改状态,即栈,所以是 `(i:xs)`。 ```haskell pop :: Stack [a] a pop = State $ \xs -> case xs of [] -> error "qaq" (x:xs) -> (x, xs) ``` `pop` 动作会从栈中弹出一个值并返回,所以结果位置是 `x`,即栈顶,并且也会修改状态,因为从栈中弹出元素了,所以状态位置是 `xs`,即除栈顶以外的元素。 那要如何使用呢? ```haskell wonderfulStack :: State [Int] () wonderfulStack = do push 1 v <- pop push $ v + 1 v' <- pop push $ v + 2 ``` 优雅了很多,不是吗? 但我们最后拿到的是一个 `State [a] ()`,而不是一个代表着栈的列表,所以需要用到 `runState` 或者 `evalState`: ```haskell evalState :: State s a -> s -> a -- evalState = (fst .) . runState -- 请允许我失礼地 pointfree 一下 evalState state s = fst (runState state s) stack :: [a] stack = evalStack wonderfulStack [] ``` `evalStack` 向 State 中传入了 来自未来 的状态,也是初始状态,进行了一系列的运算之后,拿到了最终的值。 ## 不,这不够函数式! 对于 State,Haskell 提供了更高层次的抽象:MonadState ```haskell class (Monad m) => MonadState s m | m -> s where get :: m s put :: s -> m () instance MonadState s (State s) where get = State $ \s -> (s, s) -- put = State . const . (,) () put s = State $ \_ -> ((), s) ``` `get` 是获取当前状态,而 `put` 则是修改当前的状态,来看看要怎么用这两个函数: ```haskell push' :: a -> State [a] () push' i = do xs <- get put (i:xs) pop' :: State [a] a pop' = do xs <- get case xs of [] -> error "qaq" (x:xs) -> put xs >> return x ``` 看起来好像比之前的要长,但很明显更有可读性。 ## 我的构造器呢?这么大的构造器呢? `State` 被包含在 mtl 库中的 `Control.Monad.State` 包中。但读者在使用这个 `State` 的时候会发现: ```plain > State $ \(x:xs) -> (x, xs) <interactive>:22:1: error: • Data constructor not in scope: State :: ([a0] -> (a0, [a0])) -> t • Perhaps you meant one of these: ‘StateT’ (imported from Control.Monad.State), variable ‘state’ (imported from Control.Monad.State) ``` 我们无法直接使用 `State` 这个构造器,而 GHCi 给了我们两个解决方案:神秘的 `StateT` 和 `state` 函数。 `state` 函数其实是在 MonadState 中定义的: ```haskell class Monad m => MonadState s (m :: * -> *) | m -> s where ... state :: (s -> (a, s)) -> m a ``` 而接下来要讲的,就是神秘的 `StateT`。 ## 次时代的 State 想一想,之前用 State 实现的 栈 有什么不足?是的,`pop` 函数无法处理空栈的情况,而是无奈地抛出一个错误,如果只是单纯地组合 State 和 Maybe: ```haskell push :: a -> State [a] () push = modify . (:) -- 等价于 -- push i = State $ \s -> ((), i:s) pop :: State [a] (Maybe a) pop = do xs <- get case xs of [] -> return Nothing (x:xs) -> put xs >> return (Just x) ``` 看起来没问题,让我们试试看。 ```haskell stack :: State [Int] (Maybe ()) stack = do push 1 v <- pop xs <- get case v of Nothing -> return Nothing Just v' -> do push (v' + 1) v'' <- pop xs' <- get case v'' of Nothing -> return Nothing Just v''' -> do push $ v''' + 2 return $ Just () ``` 不忍直视。 会发现又花费了大量代码在处理 Maybe 上,有没有什么办法可以自动处理这个 Maybe 呢? ## 跨时代的 StateT StateT 是一个 Monad 转换器(Monad Transformer),它可以和其他 Monad 组合,变成更强大的 Monad。 ```haskell newtype StateT s m a = StateT { runStateT :: s -> m (a, s) } deriving (Functor) ``` 和 `State` 的定义很像,只是多了个类型参数 m 而已。但就是因为这个 m,使得 StateT 拥有了和其他 Monad 组合的能力。 ```haskell instance Monad m => Monad (StateT s m) where return a = StateT $ \s -> return (a, s) (StateT m) >>= f = StateT $ \s -> do (v, s') <- m s let (StateT g) <- f v g s' instance Monad m => Monad (StateT s m) where pure = return (<*>) = ap ``` 定义也是十分相似,接下来看看 StateT 是如何解决重复检查 Nothing 值的问题的。 ```haskell push :: a -> StateT [a] Maybe () push = modify . (:) pop :: StateT [a] Maybe a pop = do xs <- get case xs of [] -> StateT $ \_ -> Nothing (x:xs) -> put xs >> return x ``` 甚至实现也和 State 的极度相似,再试试这个 StateT 版本的栈。 ```haskell stack :: StateT [Int] Maybe () stack = do push 1 v <- pop push $ v + 1 v <- pop push $ v + 2 ``` 十分优雅和熟悉的使用方式,只不过返回值和 State 的有些不一样: ```haskell -- State 和 Maybe 组合 > runState stack [] (Just (), [4]) -- StateT 和 Maybe 复合 Just ((), [4]) ``` 不过,Maybe 似乎也是一个 Monad,有没有 MaybeT 呢? ```haskell -- 省略 push, pop, stack 的实现,因为除了签名为 MaybeT (State [a]) 以外几乎没有区别 > runState (runMaybeT stack) [] (Just (), [4]) ``` 返回值和单纯把 State 与 Maybe 组合完全一致了。来看一看 MaybeT 的定义: ```haskell newtype MaybeT m a = MaybeT { runMaybeT :: m (Maybe a) } ``` 而在上面的代码中,这里的 m 是 `(State [a])`,最终类型则是 `State [a] (Maybe a)`,和单纯组合 State 与 Maybe 的签名一致了,不过多出来的强大功能则是自动处理了 Nothing 值。 ## 先有 State 还是先有 StateT 不知道读者知不知道 `Identity` 这个 Monad。 ```haskell newtype Identity a = Identity { runIdentity :: a } ``` `Identity` 只是单纯地包含了一个值,并不做任何运算: ```haskell > Identity 1 >>= \i -> (Identity i + 2) Identity 3 > (+2) 1 3 ``` 如果试着把 StateT 与 Identity 复合,那么我们得到的 `runStateT` 类型将是:`s -> Identity (a, s)` 但 Identity 只是简单地包含一个类型,`s -> Identity (a, s)` 和 `s -> (a, s)` 没有实质上的区别,或者说,这两者是**同构**的。 有好奇心的读者可能会对 State 在 GHCi 使用 `:i` 来查看定义: ```haskell > :t State type State s = StateT s Data.Functor.Identity.Identity :: * -> * ``` 没错,之前所介绍的 State 就是 StateT 和 Identity 复合的结果。 ## 抽象,都可以抽象 像 State(其实是 StateT)有 MonadState 抽象一样,Monad Transformer 也有更高一层抽象的类型类。 ```haskell class MonadTrans (t :: (* -> *) -> * -> *) where lift :: Monad m => m a -> t m a ``` `lift` 函数接收一个 Monad,并把它提升(复合)到更高层次的 Monad,比如 StateT 与 Maybe 的复合: ```haskell fail' :: StateT s Maybe a -- fail' = StateT $ \_ -> Nothing fail' = lift Nothing ``` 这样,就能把 StateT 和 Maybe 复合的 pop 中的 `Nothing -> StateT $ \_ -> Nothing` 代替为 `Nothing -> lift Nothing`。 不过回头想一想,单纯组合 State 与 Maybe 中,处理 Nothing 值使用的是 `Nothing -> return Nothing`,再看看 `return` 和 `lift` 的类型签名: ```haskell return :: Monad m => a -> m a lift :: (MonadTrans t, Monad m) => m a -> t m a ``` `lift` 就像是 Monad Transformer 中的 `return`。 ## 最后 State 和 StateT 只是一个 Monad 的冰山一角,在 Haskell 中还有更多未知等待着我们去发现和开拓。 ## 注意 所有 `deriving (Functor)` 需要 `DeriveFunctor` 扩展