【问题标题】:Why is the bind operator (>>=) defined as it is?为什么绑定运算符(>>=)按原样定义?
【发布时间】:2020-09-30 03:36:58
【问题描述】:

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

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

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

【问题讨论】:

  • 您的运营商存在and is called (>=>)。正如您所怀疑的,它可以用(>>=) 来定义,反之亦然。 (>>=)(>=>) 更常用,因为在实践中,它在大多数情况下往往更方便。相关讨论见this answer,以及this one 的第一部分(尽管反过来看问题)。
  • 简而言之,组合与应用类似。两者都很有用,但应用程序通常在编程中更常见。
  • Awesome... 应用程序语法在大多数情况下应该更方便是有道理的。
  • 我会说你的提议(被称为“kleisli composition”(<=<))实际上比(>>=)更基本,从数学上讲,(>>=)主要是程序员偏向的方便。

标签: haskell bind monads


【解决方案1】:

您可以搜索您的运营商on Hoogle,并看到它被称为(>=>)。它在(>>=) 方面的定义是quite simple

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

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

【讨论】:

  • 这是有道理的,感谢您提供两个运算符之间的明确联系。
  • 这应该是一个单独的问题,但>>= 定义时是否考虑了do 符号?
【解决方案2】:

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

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

h x y = f (g x y)

使用函数应用程序,这完全是直截了当的。只使用函数组合的版本是什么样的?

h = (f .) . g

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

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

f :: a -> b
x :: a

应用运营商应满足

h = x & f :: b

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

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

说到 monad,假设我的函数和参数是 monadic:

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

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

h = x >>= f :: m b

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

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

【讨论】:

  • 感谢您明确区分这两个运算符的用途。 “如果你第一次看到这个时不必停下来凝视一两分钟,你可能实际上是一台电脑。”我也很欣慰地注意到我仍然是人类(尽管学习了 Haskell):)
  • 我并不真正认同本质主义论点“我们的大脑天生就可以更好地使用开箱即用的名称和函数应用程序”,因为对初学者程序员的研究经常发现变量存在很大困难,函数应用和一般的值级编程,因为自然语言更倾向于函数级、连续和面向事件。我确实相信我们的大脑被我们使用的语言所连接出于熟悉的原因更喜欢这种风格。
  • 呃,我是唯一一个看不到点与无点的类型如何排列的人?在第一个 f 应用于一个值(g x y 的结果;所以假设 g::a->b->c,f 必须是 c->d)。在第二个中, f 应用于恰好是函数 . 的值,对吗?所以这两个 f 不能具有相同的类型,除非 g x y 具有与 (.) 相同的类型。这对我来说似乎是一种误导(如果我完全理解的话)。或者这是 (f .) 不意味着 f 应用于 . 的结果? [在 sml 中,(f o) 和 (f (op o)) 之间存在差异;如果同样的区别在这里适用,那么 (f .) 是什么意思?]
  • @D.BenKnoble f (.)f 应用于(.)(f .)\h -> f . h 的简写(大致上),即(f .)(.) 应用于f。这称为一个部分。还有一个翻转部分:(. f) 表示\h -> h . f。此语法特定于中缀运算符。代替h = (f .) . g,我可以写h = (.) ((.) f) g 以使其解析方式尽可能明确。
【解决方案3】:

我同意以( >=> ) :: ( 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 )

【讨论】:

  • 回复。我说 Monad 实例在 Kleisli 类别中更容易理解:例如对于任何“状态”S,状态 monad 将变为 (X x S) -&gt; (X x S)。见youtu.be/…
  • 很高兴看到所有这些其他功能都是根据一个更基本的单一基本功能来定义的。按照你的建议,我想如果我看到一个令人困惑的 Monad,我应该尝试用 >=> 重写它,看看这是否能说明问题。
  • 我不认为这是原因。 (&gt;&gt;=) 也可以派生自 (&gt;=&gt;): m &gt;&gt;= f = (const m &gt;=&gt; f) ()。所以两者都不是“更普遍的”。
  • 你是对的@DanielWagner。我不知道&gt;=&gt; 的这个定义。
  • ... 和 &gt;&gt;= 可以简化为著名的 joinfmap: m &gt;&gt;= k = join (fmap k m)
【解决方案4】:

(&gt;&gt;=) 不是组合运算符。它是一个应用程序运算符。

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

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

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

对于组合,我们有(&lt;=&lt;)(&gt;=&gt;)(同样来自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

(&gt;=&gt;) 只是 (&lt;=&lt;),其参数被翻转;(&gt;=&gt;) = flip (&lt;=&lt;)


在我们比较类型时,您可能想看看 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(=&lt;&lt;) 采用不同类型的函数,但将它们都应用于相同类型的参数(尽管方式不同)。

【讨论】:

  • 这是一个非常有用的总结。谢谢!
  • &gt;&gt;= 的参数顺序是否类似于延续传递风格而不是函数应用?
  • 为普通读者提供的附注,另请参阅these 之前的answers(免责声明,由我撰写),其中包含更多措辞甚至是插图。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2013-11-24
  • 2013-12-04
  • 1970-01-01
  • 2013-06-06
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多