非 IO monad 是否有可能违反 Haskell 中的关联性?

Cha*_*eow 7 haskell functional-programming

我是 Haskell 的新手,并试图更好地了解 monad 的结合律,其中规定:

m >>= (\x -> kx >>= h) = (m >>= k) >>= h

通过试图构建会违反该法律的虚假 monad。在高层次上,这条定律似乎是在强制对我应用“一元函数”(不确定是否有合适的术语)的关联性。特别是,它看起来像是在强制执行某些版本的 (fg) x = f(g(x)),转换为 monad。

然而,如果我们认为 f 和 g 是纯数学函数,那么这种关系似乎是微不足道的。这让我想知道如果不涉及 IO 是否有可能违反这条法律,因为以我目前对 Haskell 的基本了解,这将确保 f 和 g 没有副作用。

我希望有人能详细说明我的总体思路是否正确,或者我是否有一些关键的误解,并且可以在不使用 IO 的情况下构建这样一个反例。

Dan*_*ner 9

我们集体历史中的一个标准例子是 ListT monad 转换器,它出人意料地很难*正确。wiki对明显的实现出了什么问题以及如何修复它进行了全面的讨论,我将在此处摘录。它使用以下看起来非常合理的“monad”实例

newtype ListT m a = ListT { runListT :: m [a] }
instance (Monad m) => Monad (ListT m) where
    return a = ListT $ return [a]
    m >>= k  = ListT $ do
        a <- runListT m
        b <- mapM (runListT . k) a
        return (concat b)

instance (Monad m) => MonadPlus (ListT m) where
    mzero       = ListT $ return []
    m `mplus` n = ListT $ do
        a <- runListT m
        b <- runListT n
        return (a ++ b)
Run Code Online (Sandbox Code Playgroud)

这是关联的反例:

a,b,c :: ListT IO ()
[a,b,c] = map (liftIO . putChar) ['a','b','c']

t1 :: ListT IO ()
t1 = ((a `mplus` a) >> b) >> c

t2 :: ListT IO ()
t2 = (a `mplus` a) >> (b >> c)
Run Code Online (Sandbox Code Playgroud)

...运行runListT t1打印“aabbcc”,而runListT t2打印“aabcbc”。

这个特殊的例子碰巧使用了IO,但它不是基本的;我们也可以使用ListT (Writer String)“monad”而tell不是putChar.

*这个困难导致我们拥有一个庞大的密切相关的图书馆家族;该系列包括 list-t、logict、iteratee、machines、pipe、conduit 和 streamly。


Ben*_*Ben 7

正如卡尔在评论中提到的,结合律1的纯等价物是:

f . (g . h) = (f . g) . h
Run Code Online (Sandbox Code Playgroud)

当以这种方式表达时,一元结合律更明显地相关2

f <=< (g <=< h) = (f <=< g) <=< h
Run Code Online (Sandbox Code Playgroud)

在这种形式下fgh都是所谓的“Kleisi箭”,它们与象型功能,这方法a -> m b(对于一些amb,你不必是多态是一个Kleisi箭头)。箭头(或“鱼”)运算符3被称为“Kleisi 组合”,是对正常函数组合的完美一元类比。

(.)   :: (b ->   c) -> (a ->   b) -> (a ->   c)
(<=<) :: (b -> m c) -> (a -> m b) -> (a -> m c)
Run Code Online (Sandbox Code Playgroud)

现在,正如您所指出的,结合律对于纯函数是“微不足道的”。但那是因为只有一个函数组合运算符.,而且它实际上是结合的!结合性是运算符的属性,而不是函数;任何运营商?是关联的当且仅当f ? (g ? h) = (f ? g) ? h对于所有可能的 f、g 和 h。对于纯组合,我们只考虑一个 operator .,因此没有“搜索空间”来探索纯组合如何是非关联的。

但是每个单独的 monad 都有自己的定义<=<(通过>>=Monad 实例中的定义),所以这个问题看起来更有趣。事实上,单子关联有点从关联一个不同的野兽的财产.。的结合.性只是一个事实。对于 Haskell 程序员来说,monad 结合律是一种义务,而不是数学真理4。这是我们在编写Monad实例时必须确保坚持的事情。


现在,让我们尝试通过构建一个反例来打破它。我们想要一些错误的 monad M,例如对于某些 f、g 和 h:

f <=< (g <=< h) ? (f <=< g) <=< h
Run Code Online (Sandbox Code Playgroud)

由于我们不等式的两边都是函数,我们可以通过找到函数产生不同结果的某个值来证明它们不相等。即我们真正想要的是(对于某些 f、g、h 和 x):

(f <=< (g <=< h)) x ? ((f <=< g) <=< h) x
Run Code Online (Sandbox Code Playgroud)

提醒一下,所涉及的事物类型将是:

x :: z
h :: z -> M a
g :: a -> M b
f :: b -> M c
Run Code Online (Sandbox Code Playgroud)

现在,如果我们的“monad”M只是一个单一值 ( data M a = M a)的无聊包装,那么<=<最终的行为就像.(只是额外的包装和解包),但我们知道这. 关联的,所以我们不能想出一个这样的反例。

M如果我们要打破 的结合性,我们需要更多的结构<=<。因此,让我们尝试类似的方法data M a = M a Integer,即带有额外整数的值类型。

所以我们的<=<运算符定义必须是这种形式5

(<=<) :: (b -> M c) -> (a -> M b) -> (a -> M c)
f <=< g
  = \a -> let M b gInt = g a
              M c fInt = f b
           in M c (fInt ? gInt)
Run Code Online (Sandbox Code Playgroud)

在哪里 ?可以是Integer -> Integer -> Integer我们喜欢的任何函数6

因此,做出这些选择后,让我们看看我们是否可以稍微处理一下我们原来的不等式。左手边:

(f <=< (g <=< h)) x

-- apply definition of <=< to outer expression
= (\a -> let M b ghInt = (g <=< h) a
             M c fInt = f b
          in M c (fInt ? ghInt)) x

-- apply lambda to x
= let M b ghInt = (g <=< h) x
      M c fInt = f b
   in M c (fInt ? ghInt)

-- apply definition of <=< to inner expression
= let M b ghInt = (\a' -> let M b' hInt = h a'
                              M c' gInt = g b'
                           in M c' (gInt ? hInt))  x
      M c fInt = f b
   in M c (fInt ? ghInt)

-- apply lambda to x
= let M b ghInt = let M b' hInt = h x
                      M c' gInt = g b'
                   in M c' (gInt ? hInt))
      M c fInt = f b
   in M c (fInt ? ghInt)

-- move inner let bindings to outer let
= let M b' hInt = h x
      M c' gInt = g b'
      M b ghInt = M c' (gInt ? hInt))
      M c fInt = f b
   in M c (fInt ? ghInt)

-- since M b ghInt = M c' (gInt ? hInt), apply b = c' and ghInt = (gInt ? hInt)
= let M b' hInt = h x
      M c' gInt = g b'
      M c fInt = f c'
   in M c (fInt ? (gInt ? hInt))

-- rename b' -> a, c' -> b, for ease of reading
= let M a hInt = h x
      M b gInt = g a
      M c fInt = f b
   in M c (fInt ? (gInt ? hInt))
Run Code Online (Sandbox Code Playgroud)

现在让我们试试不等式的右手边:

((f <=< g) <=< h) x

-- apply definition of <=< to outer expression
= (\a -> let M b hInt = h a
             M c fgInt = (f <=< g) b
          in M c (fgInt ? hInt)) x

-- apply lambda to x
= let M b hInt = h x
      M c fgInt = (f <=< g) b
   in M c (fgInt ? hInt)

-- apply definition of <=< to inner expression
= let M b hInt = h x
      M c fgInt = (\a' -> let M b' gInt = g a'
                              M c' fInt = f b'
                           in M c' (fInt ? gInt)) b
   in M c (fgInt ? hInt)

-- apply lambda to b
= let M b hInt = h x
      M c fgInt = let M b' gInt = g b
                      M c' fInt = f b'
                   in M c' (fInt ? gInt)
   in M c (fgInt ? hInt)

-- move inner let bindings to outer let
= let M b hInt = h x
      M b' gInt = g b
      M c' fInt = f b'
      M c fgInt = M c' (fInt ? gInt)
   in M c (fgInt ? hInt)

-- since M c fgInt = M c' (fInt ? gInt), apply c = c' and fgInt = (fInt ? gInt)
= let M b hInt = h x
      M b' gInt = g b
      M c' fInt = f b'
   in M c' ((fInt ? gInt) ? hInt)

-- rename b -> a, b' -> b, and c' -> c, for ease of reading
= let M a hInt = h x
      M b gInt = g a
      M c fInt = f b
   in M c ((fInt ? gInt) ? hInt)
Run Code Online (Sandbox Code Playgroud)

因此,将双方放在一起,我们得出了以下结论:

let M a hInt = h x
    M b gInt = g a
    M c fInt = f b
 in M c (fInt ? (gInt ? hInt))
?
let M a hInt = h x
    M b gInt = g a
    M c fInt = f b
 in M c ((fInt ? gInt) ? hInt)
Run Code Online (Sandbox Code Playgroud)

仔细比较,您会发现除了final 的第二个参数M(“侧面的整数”)之外,一切都完全相同。而这一切与对 f、g、h 和 x(或者实际上,它们的类型是什么)做出的特定选择无关;事实证明,我们不需要知道,无论如何,我们的M<=<定义都产生了这种形式。(如果我们只包装一个值,你应该能够明白我的意思,如在data M a = M a; 在这种情况下,类型迫使我们具有关联性,因为我们最终只是实现了与纯组合.运算符相同的东西)

我们实际上还没有决定什么?操作还没有,但比较fInt ? (gInt ? hInt)vs(fInt ? gInt) ? hInt只是关联性的定义!因此,我们要让不等式成立(因此要使M's<=<是非关联的)是整数上的任何非关联运算符。减法和除法都是非关联的;我相信你可以想出无数其他的。

因此,对于这种形式7(带有额外整数的值)的提议的 monad 来说,实际上很难通过关联性要求;我们必须仔细确保我们以关联的方式组合整数(因为有很多非关联的整数运算)。任何非关联操作都会产生这种形式的假单子。

显然,可以是 monad 的类型有各种各样的形式,我只是为了找到一个简单的反例(实际上找到了整个系列)而跳到了一个非常简单的形式。但是我们在这个表格中发现的内容确实适用于更普遍的情况;的关联性<=<不会在您编写Monad实例时自动发生,您必须仔细确保它成立。很可能(而且很容易)违反这条法律。关联性通常会很自然地从类型中脱离出来,这会让您误以为它总是成立,但事实并非如此。


1而不是(f.g) x = f(g(x)),正如问题中所猜测的那样。

2>>=您用来直接创建 Monad 实例的运算符而言,该定律是:m >>= (\x -> k x >>= h) = (m >>= k) >>= h,它是等效的,但由于需要显式编写 lambda,因此看起来有点笨拙。

3你可以<=<在你的 Haskell 代码中使用这个运算符;它在Control.Monad 中

4作为程序员,我们强制要求 monad 结合性,因为 monad 的结合性实际上是一个数学事实。在数学中,如果<=<您正在查看的操作符不是关联的,它只是表明您所拥有的不是 monad。在 Haskell 中,没有什么能阻止你编码一个Monad实例,而是>>=以一种<=<非关联的方式定义。但是我们想使用数学单子的理论来编写适用于所有Monads 的通用代码;结合性义务确保我们的Monads 也是数学单子。

5这意味着>>=在 Monad 实例中一定是这样的:

(M x i) >>= f
  = let M y j = f x
     in M y (j ? i)
Run Code Online (Sandbox Code Playgroud)

6希望你可以看到abc不得不去的地方,他们做的,因为<=<是多态的,并且没有其他的方式来获得类型的值c的最终结果,但fIntgInt值是已知的Integer,所以我们可以做任何我们喜欢他们;我们甚至可以通过选择类似的东西来组成一个任意的返回值_ ? _ = 37

7您可能已经注意到,这种形式与 Writer monad 的形式几乎完全相同,只是我锁定了“log”值Integer而不是将其作为类型参数保留。我实际上并不需要这样做,因为我也没有使用Integer(除了非关联操作的存在之外)的任何属性,我只是想避免添加更多变量!但这正是 Writer monad 实例Monoid对 log 值有约束的原因;它需要知道用于组合对数值的关联操作,以便传递 monad 的关联性要求(以及需要类似mempty“初始化” pure/ 中的对数值之类的东西return)。

  • 可能值得一提的是:也可以以一种满足恒等律但仍然违反结合律的方式来制作⊕。例如,采用任何现有的非关联运算 ⊖ 并通过定义 `addZero a | 来连接恒等式 0。a &lt; 0 = a+1 | 否则=a;0 ⊕ y = y;x ⊕ 0 = x;x ⊕ y = addZero x ⊖ addZero y`。对于“大多数”非关联运算,最后一个子句甚至可以简单地是“x ⊕ y = x ⊖ y”。 (3认同)
  • @IvenMarquardt 值得记住的是,约定不仅仅是为了满足数学,因为我们喜欢数学;数学很有用!有很多代码假设所有“Monad”实例都满足关联性属性(monad 通用的东西,如 monad 转换器)。如果您使用不具有关联性的“Monad”,其中一些会被破坏。毕竟,当您需要一个类型为 `(b -&gt; M c) -&gt; (a -&gt; M b) -&gt; (a -&gt; M c)` 的非关联函数作为您的类型 `M` 时,就没有什么了毕竟,阻止您定义自己的运算符,该运算符*不是* kleisi 组合运算符。 (3认同)