我正在使用Control.Monad.Writer.Lazy
作为编写者monad用(,) [String]
编写代码。但是我发现(>>=)
和(>>)
对monoid运算符太严格了吗?例如,它们会导致无限循环:
type Wrtr a = ([String], a)
writer (x, w) = (w, x)
main :: IO ()
main = do
let one = writer ((), ["goodbye"])
let w = foldr1 (>>) $ repeat one
let (log, _) = w
mapM_ putStrLn . take 5 $ log
此代码将无限循环,并且从不打印任何内容,这对我不利。因此,现在我正在使用同一monad的这种天真的实现,这似乎很好而且很懒:
data Writer w a = Writer w a
instance Functor (Writer w) where
fmap f (Writer w x) = Writer w (f x)
instance Monoid w => Applicative (Writer w) where
pure x = Writer mempty x
(Writer w1 f) <*> (Writer w2 x) = Writer (w1 <> w2) (f x)
instance Monoid w => Monad (Writer w) where
return = pure
(Writer w1 x) >>= f =
let (Writer w2 y) = f x
in Writer (w1 <> w2) y
writer (x, w) = Writer w x
(由于类约束限制,您必须定义函子和应用实例)
如果您随后使用与上述完全相同的main
函数运行代码,它将打印5次“再见”并退出。
问题是:元组为什么这么严格?或者,如果不严格,它是什么?为什么在那里?
顺便说一句,我使用的是ghc 8.6.4,而栈lts-13.19附带的其他所有内容
答案 0 :(得分:5)
那是因为您的Writer
违反了monad法。看看这条法则:
-- forall (f :: a -> Writer m b) (x :: a).
return x >>= f = f x
-- basically f (id x) = f x, which we should agree is pretty important!
但是,可惜,它不成立! let f = undefined
! (或f = const undefined
;如果您不使用seq
,则无法区分它们)
return x >>= undefined
= Writer mempty x >>= undefined
= let (Writer m y) = undefined
in Writer (mempty <> m) y
= Writer (mempty <> undefined) undefined
根据法律,
return x >>= undefined
= undefined x
= undefined
这些不是等效的,因此您的Monad
惰性实例是非法的(是的,我相信mtl
中的实例也是非法的)。但是,Fast and Loose Reasoning is Morally Correct因此,我们通常只接受它。这个想法是,只要您保持无穷或最低值,懒惰的Writer
monad通常会遵循其应遵循的定律,但是在那些极端情况下它会崩溃。相反,严格执行完全是合法的,这就是base
中使用它的原因。但是,正如您所发现的,当懒惰的Writer
违反法律时,它们会以一种有用的方式做到这一点,因此我们将懒惰的实现放在mtl
中。
这里是demo of this behavior。请注意,惰性版本在爆炸之前会在输出中生成Writer "
,而严格版本和法律给出的规范都不会这样做。
答案 1 :(得分:4)
instance Monoid a => Monad ((,) a) where
(u, a) >>= k = case k a of (v, b) -> (u <> v, b)
这意味着它们由于case
而严格(不同于您的let (Writer w2 y) = f x
):
foldr1 (>>) $ repeat one
= one >> foldr1 (>>) (repeat one)
= (["goodbye"], ()) >>= \_ -> foldr1 (>>) (repeat one)
= case ((\_ -> foldr1 (>>) (repeat one)) ()) of (v, b) -> (["goodbye"] <> v, b)
= case (foldr1 (>>) (repeat one)) of (v, b) -> (["goodbye"] <> v, b)
这实际上会在您访问(foldr1 (>>) ...)
部分之前强制嵌套["goodbye"] <> v
。
这是因为强制进行case
模式匹配,但是let
模式是惰性的。您的代码实际上将上面的代码写为
= let (v, b) = foldr1 (>>) (repeat one) in (["goodbye"] <> v, b)
一切都很好,很懒惰。