最近又在学习 Monad,这里重新学习一下 State,以及学习功能更受限的 Reader,Writer,不考虑 Monad Transformer 的话题。
关于 State Monad 的引入已经提了很多次了,最典型的例子是使用种子的随机数生成器,其相关操作均满足seed => (returnValue, newSeed)
(seed 即 state)的形式,而将其定义为 Monad,能将种子或状态的传递过程隐藏起来,避免类似这样的难看代码:
(相关资料图)
观察函数 next3Int,可以发现其形式似乎是递归的,考虑把第一层以外的 let 抽象为(不使用闭包的)函数,可以得到:
观察这里的函数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 的形式完全一样:
这个bind
的逻辑可以这样描述:现在有一个初始状态,将其应用到第一个 State,得到结果和新的状态,将结果应用给函数 f,得到新的 State(这允许函数 f 能看到这个结果),再将新的状态应用到这个新的 State。
next3Int
可以使用 bind
去描述,在某些语言里写惯了 flatMap 的话这个还是蛮容易接受的:
将上面这个形式去抽象,就得到了 Haskell 中State
的定义,其中(State s)
将成为 Monad 的实例,>>=
即为bind
。
但上面的方法还不足以让 State 能满足生产实践,还缺少两个重要的原语——获取状态和设置状态,使用这两个原语,可以定义更多操作。
State 可以用来模拟全局变量,下面是一个非常简单的交互式控制台程序,其维护两个计数器,提供 inc 和 show 命令;这里的 State Global (IO ()) 可以看作是Global -> (Global, IO ())
,区别在于用户不需要显式地通过递归去把结果的 Global 去返回了:
State 虽好,但给了使用者太多的自由度——用户既能读也能写,这既增加了对其的理解和维护负担(想想 Scala 的 var 和 val,js 的 let 和 const),也可能导致更多逻辑上的 bug。
Reader 和 Writer 可以认为是 State 增加了相应约束——Reader 只允许读取,Writer 只允许写入。Reader 非常适合用来保存配置信息,或者进行依赖注入。
Reader 的定义去参考 State 的话是容易想到的——State 允许状态改变,但 Reader 不允许状态改变,从 State 的上下文来看,这就是说对s -> (a, s)
,返回值元组第二个参数即新的状态必定是和原状态一致,这样我们大可以省略掉第二个元素;而这就是 Reader 的定义:
Reader 的原语只有一个,称为ask
,即获取当前配置:
但 Reader 所保存的值通常并非是原子的,很可能是一个记录,这时候提供从记录中获取部分字段的方法也是比较有意义的,这里有一个新方法称为asks
去解决该问题,下面是定义和用例:
但上面仍有一个问题——这里每次都是把整个配置全都传递给各个函数,对特定函数,其中可能有很多不需要使用的配置;这会影响程序的耦合性(最小知道原则!),这时候我们可以把 Reader 所保存的信息也给修改;该操作称为 withReader,下面是定义和用例:
这个定义有点反直觉,但我们手动把 newtype 解包装的话查看签名(r -> r') -> (r' -> a) -> (r -> a)
,就会发现这里只有一种组合方式——(r'->a).(r->r')=(r->a)
,至于为何如此,写一个用例就能感觉到了:
withReader 也可以用来临时地去修改 r 的值(作用范围显然仅限于第二个参数这个 Reader 中)的同时保持 r 的类型不变,这个操作也叫 local,其函数体和 withReader 一致,但作用域更狭窄,因此应尽量使用它:
Writer 限制无法读,只能写(实际上是“拼接”),从type State s a = s -> (a, s)
出发,就是说这里的函数参数 s 是无法获取到的,因此得到这样的定义,w 为写出的内容,a 为副产品,w 的维护将被隐藏:
为什么连函数都丢掉了,直接是(w, a)
呢?因为 Haskell 是惰性求值的,用 Scala 的话来说,这里是=> (=> w, => a)
(是这样吗?),没有什么问题。
下面是 Writer 的 Monad 实例的定义,这里展示了 Writer 和 State,Reader 一个非常不同的区别——Writer 的类型参数 w 必须是幺半群 Monoid(即(集合,集合上元素的二元运算,单位元)这样一个三元组,典型例子如(字符串,字符串拼接,空字符串),(自然数,加法,零)),不然 pure 没法定义了;这显然也影响了 Writer Monad 的运算的定义——去使用该二运运算去“拼接”每一次运算的 w 作为最终结果:
好奇有没有不使用幺半群的解决方案,这样或许会得到一个不同的 Writer Monad 实现?
Writer 的原语称为 tell(和 Reader 的 ask 对应),它的实现是显然的:
基于 Writer 的 w 是幺半群这个特性,很容易想到,它可以用来进行累加和累乘,Control.Monad.Writer
包下为这两种情形定义了相应的幺半群 Sum 和 Product:
关键词: LOCAL STATE GLOBAL MONAD SCALA PURE TELL HASKELL show SEED STRING 这就是说 只有一个 解决方案