haskell, IO Monoid 关联性被破坏了吗?

haskell, is IO Monoid associativity broken?

在haskell中IO类型有Monoid的实例:

instance Monoid a => Monoid (IO a) where
    mempty = pure empty

如果我有三个动作共享一些状态,并通过副作用改变彼此的行为,这可能会导致违反关联律,从 IO 类型的角度来看:

a1:: IO String
a2:: IO String
a3:: IO String

(a1 mappend a2) mappend a3 /= a1 mappend (a2 mappend a3)

例如,如果 a1、a2、a3 以字符串形式请求当前时间,或者 IO 包含一些计算请求编号的 DB。这意味着它可以是:

(a1 `mappend` a2) `mappend` a3  == "1"++"2"++"3"
a1 `mappend` (a2 `mappend` a3) == "3"++"1"++"2"

编辑:

我想我不应该举一个数据库的例子,它很混乱, 更喜欢的例子:

a1 = show <$> getUnixTime 
a2 = show <$> getUnixTime
a3 = show <$> getUnixTime

l = (a1 `mappend` a2) `mappend` a3
r = a1 `mappend` (a2 `mappend` a3)
liftA2 (==) l r
**False**

那么,如果 IO 类型可以打破结合律,为什么它是幺半群呢?或者我遗漏了什么?

a1 `mappend` (a2 `mappend` a3) 不按 a2a3a1 的顺序 运行。例如,与 Python 这样的命令式语言相比,在 Haskell 中,IO a 不是计算的某些 结果 ,它是 recipe 以产生 a 的值。你实际上可以看到 IO 更像是 Python 中的延续,你传递一个函数,最终它可以被调用,但你不直接调用它。

mappend 函数实现为 Semigroup a => Semigroup (IO a) 实例的 liftA2 (<>),正如我们在 source code:

中看到的
instance Semigroup a => Semigroup (IO a) where
    (<>) = liftA2 (<>)

这意味着 mappend 实现为:

mappendIO :: Semigroup a => IO a -> IO a -> IO a
mappendIO f g = do
    x <- <b>f</b>
    y <- <b>g</b>
    pure (x <> y)

所以它 运行s fg.

之前

如果我们现在查看 (a1 `mappend` a2) `mappend` a3,我们会看到:

(a1 `mappend` a2) `mappend` a3 = do
    x <- do
        x1 <- a1
        x2 <- a2
        pure (x1 <> x2)
    y <- a3
    pure (x <> y)

相当于:

(a1 `mappend` a2) `mappend` a3 = do
    x1 <- a1
    x2 <- a2
    x3 <- a3
    pure ((x1 <> x2) <> x3)

如果我们再看 a1 `mappend` (a2 `mappend` a3) 那么这就等同于:

a1 `mappend` (a2 `mappend` a3) = do
    x <- a1
    y <- do
        y1 <- a2
        y2 <- a2
        pure (y1 <> y2)
    pure (x <> y)

相当于:

a1 `mappend` (a2 `mappend` a3) = do
    x1 <- a1
    x2 <- a2
    x3 <- a2
    pure (x1 <> (x2 <> x3))

由于 x1 <> (x2 <> x3) 等同于 (x1 <> x2) <> x3,这将因此 return 两个项目的 相同 结果。

至于你的测试:

l = (a1 `mappend` a2) `mappend` a3
r = a1 `mappend` (a2 `mappend` a3)
liftA2 (==) l r
False

请注意 liftA2 (==) 将再次 定义一个序列 ,这意味着您的 liftA2 (==) l r 被定义为:

liftA2 (==) l r = do
    x1 <- a1
    x2 <- a2
    x3 <- a3
    y1 <- a1
    y2 <- a2
    y3 <- a3
    pure ((x1 <> x2) <> x3) == (y1 <> (y2 <> y3))

你因此运行rafterl.

如果你使用 State monad,你可以更清楚会发生什么,并验证规则是否被应用。但是,您需要重置 lr 之间的状态。

你不能使用liftA2 (==)来有意义地比较IO值:这个比较关系甚至不是自反的!

确实,如果我们 运行

a1 = show <$> getUnixTime 
liftA2 (==) a1 a1

有可能得到 False 结果,因为两次调用 getUnixTime 之间的时间过去了,所以 returned 值可能不同。

如果您将某个随机数生成器的值定义为 a1 到 return,这就更清楚了。调用两次几乎总是会产生不同的结果。

另一个例子:如果用户输入两行不同的内容,liftA2 (==) getLine getLine 可以 return false。

当我们说 ioAction1 等于 ioAction2 时,我们的意思是如果在完全相同的上下文中执行它们将具有相同的效果。这与在 之后 执行另一个操作并比较结果不同。

精确定义“相同的 IO 效果”是很棘手的,因为我们通常希望您忽略性能差异。例如。 return () >> print True可能比print True稍慢,如果不做优化,我们还是想把这两个动作看成是一样的效果。