为什么绑定运算符 (>>=) 是这样定义的?

bro*_*ell 69 monads haskell bind

我已经学习 Haskell 几个星期了(只是为了好玩),刚刚观看了 Brian Beckman介绍 monad的精彩视频。他激励 monad 需要创建一个更通用的组合运算符。按照这个思路,如果我有两个功能:

f :: a -> b
g :: b -> c
Run Code Online (Sandbox Code Playgroud)

组合运算符应该满足

h = g . f :: a -> c
Run Code Online (Sandbox Code Playgroud)

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

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

说到 monad,假设我有两个功能:

f :: a -> m b
g :: b -> m c
Run Code Online (Sandbox Code Playgroud)

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

h = f >>= g :: a -> m c
Run Code Online (Sandbox Code Playgroud)

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

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

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

h a = (f a) >>= g :: m c
Run Code Online (Sandbox Code Playgroud)

因此

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

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

Dan*_*ner 61

有人可以解释这种绑定定义选择背后的原因吗?

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

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

对于函数应用,这完全是简单的。只使用函数组合的版本是什么样的?

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

如果您在第一次看到这个时不必停下来凝视一两分钟,那么您实际上可能是一台计算机。

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

f :: a -> b
x :: a
Run Code Online (Sandbox Code Playgroud)

应用运营商应满足

h = x & f :: b
Run Code Online (Sandbox Code Playgroud)

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

(&) :: a -> (a -> b) -> b
Run Code Online (Sandbox Code Playgroud)

当谈到 monad 时,假设我的函数和参数是 monadic:

f :: a -> m b
x :: m a
Run Code Online (Sandbox Code Playgroud)

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

h = x >>= f :: m b
Run Code Online (Sandbox Code Playgroud)

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

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

  • 我并不真正相信本质主义的论点“我们的大脑天生就可以更好地处理开箱即用的名称和函数应用”,因为对初学者程序员的研究经常发现变量、函数应用和值级编程存在很大困难一般来说,因为自然语言往往更加功能级、连续性和面向事件。我*确实*相信我们的大脑*通过我们使用的语言*来连接,因为熟悉的原因而更喜欢这种风格。 (10认同)
  • 感谢您明确说明这两个运营商的目的之间的区别。“如果你第一次看到这个时不必停下来凝视一两分钟,那么你实际上可能是一台计算机。” 我也很欣慰地注意到我仍然是人类(尽管学习了 Haskell):) (5认同)

ama*_*loy 29

您可以在 Hoogle 上搜索您的运营商,并看到它被称为(>=>)。它在以下方面定义(>>=)相当简单

f >=> g = \x -> f x >>= g
Run Code Online (Sandbox Code Playgroud)

从某种意义上说,(>=>)它更好地反映了概括组合的想法,但我认为(>>=)作为原始运算符更有效,因为它在更多情况下更实用,并且更容易与 do-notation 相关联。


che*_*ner 24

(>>=)不是一个合成算子。它是一个应用程序操作符。

(&)   ::              a -> (a ->   b) ->   b
(>>=) :: Monad m => m a -> (a -> m b) -> m b
Run Code Online (Sandbox Code Playgroud)

还有(=<<)(from Control.Monad),它对应于更常见的应用程序运算符($)

($)   ::            (a ->   b) ->   a ->   b
(=<<) :: Monad m => (a -> m b) -> m a -> m b
Run Code Online (Sandbox Code Playgroud)

对于组合,我们有(<=<)(>=>)(同样来自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
Run Code Online (Sandbox Code Playgroud)

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


当我们比较类型时,您可能想看看如何fmap适应。

($)   ::              (a ->   b) ->   a ->   b
fmap  :: Functor f => (a ->   b) -> f a -> f b
(=<<) :: Monad m   => (a -> m b) -> m a -> m b
Run Code Online (Sandbox Code Playgroud)

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

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


mic*_*hid 6

我同意,在思维方面的( >=> ) :: ( 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
Run Code Online (Sandbox Code Playgroud)

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

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

( <*> ) mab ma =
    mab >>= \f ->
    ma  >>= \a ->
    return ( f a )
Run Code Online (Sandbox Code Playgroud)

  • 我认为不是这个原因。`(&gt;&gt;=)` 也可以从 `(&gt;=&gt;)` 导出:`m &gt;&gt;= f = (const m &gt;=&gt; f) ()`。所以两者都不是“更普遍”。 (4认同)