对于 monad,可以根据 bind 来定义 join 吗?

With monads, can join be defined in terms of bind?

在 Haskell 中,monad 是根据函数 return 和 bind 定义的,其中 return 的类型为 a -> m a,bind 的类型为 m a -> (a -> m b) -> m b .之前已经指出monads can also be defined in terms of return and join,其中join是一个类型为m (m a) -> m a的函数。 Bind 可以根据 join 来定义,但反过来可能吗? join可以用bind来定义吗?

如果没有加入,我不知道如果我以某种方式得到一个 "twice wrapped" monadic 值,m (m a) - none 函子或 monad 操作,我会怎么做"remove any layers",可以这么说。如果这不可能,为什么 Haskell 和许多其他 monad 实现根据绑定来定义它们?它似乎严格来说没有基于连接的定义有用。

是的,这很简单:

join m = m >>= id

有可能:

join :: Monad m => m (m a) -> m a
join m = (m >>= id)

注意 >>= 的棘手实例化:

(>>=) :: m b -> (b -> m c) -> m c
-- choosing b ~ m a , c ~ a
(>>=) :: m (m a) -> (m a -> m a) -> m a

所以我们可以正确地选择 id 作为第二个参数。

Bind (>>=) 事实上 "remove a layer":

(>>=) :: Monad m => m a -> (a -> m b) -> m b

直觉上它 "gets some as out of the m a",然后提供给 a -> m b 函数,然后从结果中生成一个 m b

人们通常说它需要函数参数在m中再次包装它的输出,但实际上并非如此。它要求函数的输出 包裹在 m 中的东西,但包裹来自哪里并不重要。

在实施 join 的情况下,我们从 "double-wrapped" 开始:m (m a)。我们可以将其插入到绑定的签名中,并立即找出绑定 "double-wrapped" 值时可以使用的函数类型:

m (m a) -> (m a -> m b) -> m b

现在这里与 bind 一起使用的函数将接收一个 已经 包装在 m 中的值。所以我们不需要 "re-wrap" 任何东西;如果我们 return 它未经修改,它已经是输出的正确类型。实际上是 "removed one layer of wrapping" - 这适用于除最后一层以外的任何层。

所以这告诉我们我们只需要绑定 id:

join = (>>= id)

虽然这个问题已经得到了充分的回答,但我认为对于任何 Haskell 新人来说,以下内容值得注意。

在 Haskell 中学习 monad 时首先看到的是列表的定义:

instance Monad [] where
    return x  =  [x]
    xs >>= f  =  concatMap f xs

在这里,我们看到 bind 对列表的功能等同于 concatMap,只是参数翻转了。这在检查类型时很有意义:

concatMap ::            (a ->  [b]) ->  [a] ->  [b]
bind      :: Monad m => (a -> m b ) -> m a  -> m b        -- (>>=) flips the arguments

从直觉上讲,列表的 join 的定义等同于
concat :: [[a]] -> [a].

函数的名称可能使它有点明显,但是 concat 可以通过保持内部列表原样从 concatMap 中恢复,以抵消 "map" concatMap 的一部分:

concatMap       id xs      
  = concat (map id xs)
  = concat (    id xs)
  = concat         xs      -- or simply 'concat = concatMap id'

同样的 属性 通常适用于 monads:

join x  =  x >>= id        -- or 'join = bind id'

这真的来自于

bind f m  =  join (fmap f m)

所以

bind id m  =  join (fmap id m)           -- bind id  =  join . fmap id
           =  join (     id m)           --          =  join .      id
           =  join          m            --          =  join

因为所有的 monad 首先是 Functor,并且根据 Functor 定律 fmap id === id

Can join be defined in terms of bind?

TL;DR 回答:是的。

join ∷ (Monad m) ⇒ m (m a) → m a
join = (=<<) id

较长的答案: 为了添加一些尚未提及的微妙之处,我将提供一个新答案,首先扩展 Lee's ,因为值得注意的是他们的答案可以简化。从原文开始:

join ∷ (Monad m) ⇒ m (m a) → m a
join m = m >>= id

可以找 Eta conversion (η-conversion) opportunity to make the function definition point-free。为此,我们首先要重写没有中缀 >>= 的函数定义(如果我们首先通过名称 bind 调用 >>= 可能会这样做)。

join m = (>>=) m id

现在观察如果我们使用 flip 函数,回忆一下:

-- defined in Data.Function
-- for a function of two arguments, swap their order
flip ∷ (a → b → c) → b → a → c
flip f b a = f a b

现在可以使用 flipm 置于 η 缩减的位置:

join m = (flip (>>=)) id m

应用 η 缩减:

join = (flip (>>=)) id

现在注意到 flip (>>=) 可以替换为 (=<<)(在 Control.Monad 中定义):

join = (=<<) id

我们终于可以看到更短的无点定义:

join ∷ (Monad m) ⇒ m (m a) → m a
join = (=<<) id

其中 (=<<) 的类型为:

(=<<) ∷ ∀ (m ∷ * -> *) a b. (Monad m) ⇒ (a → m b) → m a → m b

在这个过程中被实例化为:

(=<<) ∷ (m a → m a) → m (m a) → m a

此外,人们可能还会注意到,如果我们将上面的代码放回中缀形式,flip 就变成隐含的,我们得到相同的最终 as Ben

join = (>>= id)