Haskell元组monad太严格了?

时间:2019-05-02 16:41:51

标签: haskell monads lazy-evaluation

我正在使用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附带的其他所有内容

2 个答案:

答案 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)

Hackage says

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)

一切都很好,很懒惰。