关于Writer Monad的类型声明、约束及扩展的技术疑问
先看问题里的核心定义和示例代码:
newtype Writer w a = Writer { runWriter :: (a, w) } instance (Monoid w) => Monad (Writer w) where return x = Writer (x, mempty) (Writer (x,v)) >>= f = let (Writer (y, v')) = f x in Writer (y, v `mappend` v')
运行示例:
ghci> runWriter (return 3 :: Writer String Int) (3,"") ghci> runWriter (return 3 :: Writer (Sum Int) Int) (3,Sum {getSum = 0}) ghci> runWriter (return 3 :: Writer (Product Int) Int) (3,Product {getProduct = 1})
下面逐个解答你的疑问:
1. 为什么返回的是Writer String Int而非对应的元组?
因为return是Monad类型类的方法,它的签名是return :: a -> m a,这里的m是Writer String(一个接受单个类型参数的类型构造器),所以return 3返回的必然是Writer String Int类型的值——这是Monad的要求:把纯值包裹进Monad上下文里。
而(a, w)是Writer w a内部存储的结构,你需要通过runWriter这个访问器函数,才能把内部的元组提取出来。如果直接返回元组,那它就不是Monad实例了,没法参与>>=这类Monad操作。
2. 是否可以定义类似Writer2的Monad类型,使其支持Writer String Int Float这样的三个类型参数?
能定义,但这样的结构不符合Monad的设计意图,实用性很差。
首先,Haskell的Monad类型类要求类型构造器是接受单个类型参数的(即种类为* -> *)。三个参数的Writer2 w a b种类是* -> * -> * -> *,要做成Monad实例,必须先固定前两个参数,比如:
newtype Writer2 w a b = Writer2 { runWriter2 :: (b, a, w) } instance (Monoid w) => Monad (Writer2 w a) where return x = Writer2 (x, mempty, mempty) (Writer2 (x, a_val, w_val)) >>= f = let (Writer2 (y, a_val', w_val')) = f x in Writer2 (y, a_val `mappend` a_val', w_val `mappend` w_val')
但这里的问题是:Monad的>>=只能操作最后一个类型参数(上面的b),前两个参数w和a在实例中是固定的,没法作为计算的“结果”部分动态变化。Writer的核心是积累Monoid类型的日志(w)+ 返回计算结果(a),三个参数的结构完全没必要,反而会把逻辑搞复杂。
3. 为什么实例化时是(Monoid w) => Monad (Writer w)而非(Monoid w a) => Monad (Writer w a)?且w没有像Num约束中的a那样出现在Monad绑定和return方法的类型签名中?
拆解成两点说:
- 首先,Monad的实例是给类型构造器定义的,不是给具体类型。
Writer w是一个接受单个类型参数的构造器(给它传a就得到具体类型Writer w a),而Writer w a是一个具体类型,不能作为Monad实例的参数。所以实例必须写成Monad (Writer w),而不是Monad (Writer w a)。 - 其次,
Monoid是单参数类型类,Monoid w a这种写法根本不合法——Monoid只能约束一个类型。我们只需要w是Monoid,因为Writer的核心逻辑是用mempty(初始日志)和mappend(合并日志)来处理w类型的值,整个过程和a(计算结果的类型)完全无关:return只需要把a打包,同时给个空日志;>>=只需要把前后两次的日志合并,不管a是什么类型。所以a不需要任何约束,w只需要满足Monoid即可。
至于你说的“w没出现在方法签名里”,是因为Writer w已经作为Monad实例的参数固定了,方法的类型签名会自动推导:比如return的签名在Monad (Writer w)下是return :: a -> Writer w a,这里的w已经由实例约束为Monoid,不需要再写在方法签名里。
内容的提问来源于stack exchange,提问作者Piskator

