观察函数 next3Int,可以发现其形式似乎是递归的,考虑把第一层以外的 let 抽象为(不使用闭包的)函数,可以得到:
1 2 3 4 5 6 7
next3Int = \s0 -> let (x1, s1) = nextInt s0 in f x1 s1 where f :: a -> (String -> ((a, Int, Int), String)) f x1 = \s1 -> let (x2, s2) = nextInt s1 inlet (x3, s3) = nextInt s2 in ((x1, x2, x3), s3)
观察这里的函数f,可以发现,其接受了第一个 nextInt 的返回值x1后,获得了另一个形式和 nextInt 一致的函数f x1,并且整个函数的返回结果也为f x1应用 s0 的结果…这里是一个这样的逻辑:nextInt -> f -> next3Int,定义type State s a = s -> (a, s),令Int = a, (Int, Int, Int) = b, String = s, 有 State s a -> (a -> State s b) -> State s b,这就是 State 的组合方式,有趣的地方是,这玩意函数体和上面的 next3Int 的形式完全一样:
1 2
bind :: State s a -> (a -> State s b) -> State s b bind x f = \s -> let (x1, s1) = x s in f x1 s1
这个bind的逻辑可以这样描述:现在有一个初始状态,将其应用到第一个 State,得到结果和新的状态,将结果应用给函数 f,得到新的 State(这允许函数 f 能看到这个结果),再将新的状态应用到这个新的 State。
newtypeState s a = State { runState :: s -> (a, s) }
instanceFunctor (States) where -- State 的 fmap 就是在 State 执行后改变返回值,不改变当前状态 -- 可以从 Functor laws 去出发——fmap 不改变容器或上下文本身,即这里的 State s fmap :: (a -> b) -> State s a -> State s b fmap fn (State x) = State $ \s -> let (x', s') = x s in (fn x', s')
instanceApplicative (States) where pure :: a -> State s a pure x = State (x, ) -- 先用原状态 s 计算 f,得到函数 f'和状态 s1,再用状态 s1 计算 x 得到值 x'和状态 s2 -- 最终结果为 f' x',状态为 s2 (<*>) :: State s (a -> b) -> State s a -> State s b (State f) <*> (State x) = State $ \s -> let (f', s1) = f s (x', s2) = x s1 in (f' x', s2)
instanceMonad (States) where (>>=) :: State s a -> (a -> State s b) -> State s b (State x) >>= f = State $ \s -> let (x', s1) = x s in runState (f x') s1
操作
但上面的方法还不足以让 State 能满足生产实践,还缺少两个重要的原语——获取状态和设置状态,使用这两个原语,可以定义更多操作。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
-- 从 bind(或 do)的上下文中,用户只能看到 State 的返回值,因此必须把状态当作返回值去返回 get :: State s s get = State $ \s -> (s, s)
put :: s -> State s () put x = State $ \_ -> ((), x)
modify :: (s -> s) -> State s () modify f = State $ \s -> ((), f s)
incAndGet :: Num s => State s s incAndGet = modify (+ 1) >>= const get
getAndInc :: Num s => State s s getAndInc = do num <- get modify (+ 1) return num
用例
State 可以用来模拟全局变量,下面是一个非常简单的交互式控制台程序,其维护两个计数器,提供 inc 和 show 命令;这里的 State Global (IO ()) 可以看作是Global -> (Global, IO ()),区别在于用户不需要显式地通过递归去把结果的 Global 去返回了:
dataGlobal = Global { counterA :: Int, counterB :: Int }
inputHandler :: String -> StateGlobal (IO ()) inputHandler"incA" = do modify (\g -> g{counterA = g.counterA + 1}) Global{counterA=res} <- get return $ do putStrLn $ "increase counterA, res: " ++ show res inputHandler"incB" = do modify (\g -> g{counterB = g.counterB + 1}) Global{counterA=res} <- get return $ do putStrLn $ "increase counterB, res: " ++ show res inputHandler"show" = do Global{counterA, counterB} <- get return $ do putStrLn $ "counterA: " ++ show counterA ++ ", counterB: " ++ show counterB inputHandler cmd = do return . putStrLn $ "Invalid Command: " ++ cmd
loop :: Global -> IOGlobal loop g = do putStr "> " hFlush stdout -- 没有这一行的话编译运行的时候 > 会在下一行输出,这是因为通过编译运行和通过 ghci 运行时缓冲区配置不同导致的 input <- getLine let (action, newGlobal) = runState (inputHandler input) g action loop newGlobal
main :: IO () main = do putStrLn "Hello, World! Valid Command: incA, incB, show" void $ loop Global { counterA = 0, counterB = 0 }
Reader
动机
State 虽好,但给了使用者太多的自由度——用户既能读也能写,这既增加了对其的理解和维护负担(想想 Scala 的 var 和 val,js 的 let 和 const),也可能导致更多逻辑上的 bug。
Reader 和 Writer 可以认为是 State 增加了相应约束——Reader 只允许读取,Writer 只允许写入。Reader 非常适合用来保存配置信息,或者进行依赖注入。
定义
Reader 的定义去参考 State 的话是容易想到的——State 允许状态改变,但 Reader 不允许状态改变,从 State 的上下文来看,这就是说对s -> (a, s),返回值元组第二个参数即新的状态必定是和原状态一致,这样我们大可以省略掉第二个元素;而这就是 Reader 的定义:
1 2 3 4 5 6 7 8 9 10 11 12
newtypeReader r a = Reader { runReader :: r -> a }
instanceFunctor (Readerr) where fmap :: (a -> b) -> Reader r a -> Reader r b fmap fn (Reader r) = Reader $ \x -> fn $ r x
instanceApplicative (Readerr) where pure :: a -> Reader r a pure x = Reader $ const x (<*>) :: Reader r (a -> b) -> Reader r a -> Reader r b (Reader f) <*> (Reader x) = Reader $ \r -> f r $ x r
local :: (r -> r) -> Reader r a -> Reader r a local f (Reader r') = Reader $ \r -> r' $ f r
-- 假设现在有两种颜色 dataColor = Red | Blue
render :: Color -> String -> String render c s = undefined
-- 根据当前配置去渲染 colored :: String -> ReaderColorString colored str = do color <- ask return $ render color str
-- 现在想要获得与配置渲染的字体以及渲染配置颜色相反的字体 coloredAndRevertColored :: String -> ReaderColor (String, String) coloredAndRevertColored str = do -- 在这个简单情景下显然有更容易的方式… color <- local revert ask originalColor <- ask return (render color str, render originalColor str) where revert Red = Blue revert Blue = Red
Writer
Writer 限制无法读,只能写(实际上是“拼接”),从type State s a = s -> (a, s) 出发,就是说这里的函数参数 s 是无法获取到的,因此得到这样的定义,w 为写出的内容,a 为副产品,w 的维护将被隐藏:
下面是 Writer 的 Monad 实例的定义,这里展示了 Writer 和 State,Reader 一个非常不同的区别——Writer 的类型参数 w 必须是幺半群 Monoid(即(集合,集合上元素的二元运算,单位元)这样一个三元组,典型例子如(字符串,字符串拼接,空字符串),(自然数,加法,零)),不然 pure 没法定义了;这显然也影响了 Writer Monad 的运算的定义——去使用该二运运算去“拼接”每一次运算的 w 作为最终结果:
好奇有没有不使用幺半群的解决方案,这样或许会得到一个不同的 Writer Monad 实现?
1 2 3 4 5 6 7 8 9 10 11 12 13 14
instanceFunctor (Writerw) where fmap :: (a -> b) -> Writer w a -> Writer w b fmap f (Writer (w, a)) = Writer (w, f a)
instanceMonoid w => Applicative (Writerw) where pure :: a -> Writer w a pure x = Writer (mempty, x) (<*>) :: Writer w (a -> b) -> Writer w a -> Writer w b (Writer (w0, f)) <*> (Writer (w1, x)) = Writer (mappend w0 w1, f x)
instanceMonoid w => Monad (Writerw) where (>>=) :: Writer w a -> (a -> Writer w b) -> Writer w b (Writer (w, x)) >>= f = Writer (mappend w w', x') whereWriter (w', x') = f x
Writer 的原语称为 tell(和 Reader 的 ask 对应),它的实现是显然的:
1 2
tell :: w -> Writer w () tell w = Writer (w, ())
基于 Writer 的 w 是幺半群这个特性,很容易想到,它可以用来进行累加和累乘,Control.Monad.Writer包下为这两种情形定义了相应的幺半群 Sum 和 Product: