为什么绑定运算符 (>>=) 按原样定义?

Why is the bind operator (>>=) defined as it is?

我已经研究 Haskell 几个星期了(只是为了好玩),刚刚看了 Brian Beckman 的精彩 video introducing monads。他激励 monad 需要创建一个更通用的组合运算符。按照这个思路,如果我有两个函数:

f :: a -> b
g :: b -> c

合成算子应满足

h = g . f :: a -> c

由此我可以推断出 . 运算符的正确类型:

(.) : (b -> c) -> (a -> b) -> (a -> c)

说到 monad,假设我有两个函数:

f :: a -> m b
g :: b -> m c

在我看来,自然的选择是定义一个广义组合运算符,其工作方式如下:

h = f >>= g :: a -> m c

在这种情况下,>>= 运算符的类型签名为:

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

但实际上运算符似乎是这样定义的

h a = (f a) >>= g :: m c

因此

(>>=) : m b -> (b -> m c) -> m c

有人可以解释一下选择绑定定义背后的原因吗?我假设这两个选择之间存在一些简单的联系,其中一个可以用另一个来表达,但我目前没有看到。

您可以搜索您的运营商on Hoogle, and see that it is called (>=>). Its definition in terms of (>>=) is quite simple:

f >=> g = \x -> f x >>= g

在某种意义上 (>=>) 更好地反映了泛化组合的想法,但我认为 (>>=) 作为原始运算符效果更好,因为它在更多情况下实用,并且更容易关联做符号。

Could someone explain the reasoning behind this choice of definition for bind?

当然可以,这几乎与您的推理完全相同。只是...我们想要一个更通用的 application 运算符,而不是更通用的组合运算符。如果您做过很多(任何)无点编程,您会立即认识到原因:与有点程序相比,无点程序很难编写,而且难以阅读。例如:

h x y = f (g x y)

使用函数应用程序,这非常简单。只使用函数组合的版本是什么样的?

h = (f .) . g

如果您第一次看到这个时不必停下来盯着看一两分钟,那么您可能真的是一台计算机。

因此,无论出于何种原因:我们的大脑天生就可以更好地处理开箱即用的名称和函数应用程序。所以这就是你的其余论点的样子,但是应用代替了组合。如果我有一个函数和一个参数:

f :: a -> b
x :: a

应用运营商应满足

h = x & f :: b

由此我可以推断出 & 运算符的正确类型:

(&) :: a -> (a -> b) -> b

说到单子,假设我的函数和参数是单子的:

f :: a -> m b
x :: m a

自然的选择是定义一个通用的应用程序运算符,其工作方式如下:

h = x >>= f :: m b

在这种情况下,>>= 运算符的类型签名为:

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

我同意用 ( >=> ) :: ( a -> m b ) -> ( b -> m c ) -> ( a -> m c) 来思考通常感觉更自然,因为它更接近通常的函数组合,事实上它 是 Kleisli 类别中的 组合。 Haskell的很多monad实例从这个角度来看其实更容易理解。

Haskell 选择 ( >>= ) :: m a -> ( a -> m b) -> m b 的一个原因可能是这个定义在某种程度上是最通用的。 >=>join :: m ( m x ) -> m x 都可以简化为 >>=:

( >=> ) f g x = f x >>= g

join mmx = mmx >>= id

如果将 return :: x -> m x 添加到组合中,还可以导出 fmap :: ( a -> b ) -> m a -> m b(函子)和 ( <*> ) :: m ( a -> b ) -> m a -> m b(应用):

fmap f ma = ma >>= ( return . f )

( <*> ) mab ma =
    mab >>= \f ->
    ma  >>= \a ->
    return ( f a )

(>>=) 不是 组合运算符。是应用运营商。

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

还有(=<<)(来自Control.Monad),对应更常用的应用运算符($):

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

对于组合,我们有 (<=<)(>=>)(同样来自 Control.Monad,第一个完全类似于 (.):

(.)   ::            (b ->   c) -> (a ->   b) -> a ->   c
(<=<) :: Monad m => (b -> m c) -> (a -> m b) -> a -> m c
(>=>) :: Monad m => (a -> m b) -> (b -> m c) -> a -> m c

((>=>) 只是 (<=<) 的参数翻转;(>=>) = flip (<=<))


虽然我们正在比较类型,但您可能想看看 fmap 如何适合。

($)   ::              (a ->   b) ->   a ->   b
fmap  :: Functor f => (a ->   b) -> f a -> f b
(=<<) :: Monad m   => (a -> m b) -> m a -> m b

($)fmap 采用相同类型的函数,但将其应用于不同类型的参数。

fmap(=<<) 采用不同类型的函数,但将它们都应用于同一类型的参数(尽管采用不同的方式)。