【问题标题】:Haskell tuple monad is too strict?Haskell 元组单子太严格了?
【发布时间】:2019-05-02 16:41:51
【问题描述】:

我正在使用Control.Monad.Writer.Lazy 编写我的代码,并使用(,) [String] 作为我的作家monad。但是我发现(>>=)(>>)对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 函数运行代码,它将打印五次“再见”并退出。

所以问题是:为什么元组如此严格?或者如果不是严格,那是什么,为什么会存在?

顺便说一句,我正在使用 ghc 8.6.4 以及堆栈 lts-13.19 附带的所有其他内容

【问题讨论】:

  • 我不明白“我正在使用 Control.Monad.Writer.Lazy 编写代码,使用 (,) [String] 作为我的 writer monad。”如果您使用的是(,) [String] monad,那么您没有使用Control.Monad.Writer.Lazy。我猜 base monad (,) [String]Control.Monad.Writer.Strict.Writer [String] 同构,它应该在你的代码上循环(一旦适应)。
  • 我该死的我读错了。我以为这个模块只导出了实例和方法,没有注意到Writer 本身。是时候测试一下了。

标签: haskell monads lazy-evaluation


【解决方案1】:

那是因为你的 Writer 违反了单子定律。看看这条定律:

-- 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 中的原因。然而,正如您所发现的,当懒惰的Writers 违法时,他们会以一种有用的方式这样做,所以我们将懒惰的实现放在mtl 中。

这是demo of this behavior。请注意,惰性版本在炸毁之前会在输出中生成 Writer ",而严格版本和法律规定的规范都不会这样做。

【讨论】:

  • base 有一些类似的非法实例,包括Bifunctor (,)Arrow (-&gt;)。我觉得有点苦恼。
  • @dfeuer:他们有什么不合法的?对于Arrow 实例arr f = f; (f *** g) ~(x, y) = (f x, g y),惰性匹配是否使其比first (first f) &gt;&gt;&gt; arr assoc = arr assoc &gt;&gt;&gt; first f 的定义更明确? assoc ((a, b), c) = (a, (b, c)) 的定义一开始似乎很可疑。
  • @JonPurdy,好吧,公平地说,我不确定Arrow,但我肯定期望 first id = id,这并不完全满足。
  • base 中的另一个不完全单子:懒惰的ST
  • 这是一个非常好的答案,谢谢!我总是忘记底部。
【解决方案2】:

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)

这实际上会在您访问["goodbye"] &lt;&gt; v 部分之前强制嵌套(foldr1 (&gt;&gt;) ...)

这是因为 case 模式匹配是强制的,但 let 模式是惰性的。您的代码实际上将上述内容写为

= let (v, b) = foldr1 (>>) (repeat one) in (["goodbye"] <> v, b)

一切都很好,很懒。

【讨论】:

  • 哦,谢谢,我查看了源代码,但不知道case .. of 的这种行为
猜你喜欢
  • 1970-01-01
  • 2020-08-07
  • 2012-11-12
  • 1970-01-01
  • 2014-12-14
  • 1970-01-01
  • 1970-01-01
  • 2017-03-29
  • 1970-01-01
相关资源
最近更新 更多