单子绑定(>>=)运算符更接近于函数组合(链式调用)还是函数应用?

25

在我阅读的许多文章中,monad >>= 运算符被描述为表示函数组合的一种方式。但对我来说,它更接近于一种高级函数应用。

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

关于作曲,我们有以下要点:

(.)   :: (b -> c) -> (a -> b) -> a -> c
(>=>) :: Monad m => (a -> m b) -> (b -> m c) -> a -> m c
请澄清。

2
它们的翻转变体更加相似:(=<<) :: Monad m => (a -> m b) -> m a -> m b(<=<) :: (b -> m c) -> (a -> m b) -> (a -> m c) - rampion
还有一个Applicative版本:(<*>) :: Applicative m => m (a -> b) -> m a -> m bliftA2 (.) :: Applicative m => m (b -> c) -> m (a -> b) -> m (a -> c) - rampion
你的问题没有一个真正的答案。最终,单子绑定就是它本身,不多不少。话虽如此,你的观点完全是合理的——只是,与其试图在这里发现一个“真相”,也许你应该接受“组合”类比已经帮助你走了这么远(已经足够了!),而不要担心它的缺陷。 - Luis Casillas
2
一种思考方式是函数应用程序接受一个“普通类型”的值并将其应用于函数。另一方面,绑定则接受一个“装饰”类型的值m a。这个值必须由某个装饰函数(可能是return)创建。因此,绑定实际上将创建该值的函数与另一个函数(续集)组合起来。应用程序也可以用于组合,但它也可以作用于文字。当然,文字可以被替换为一个取单位的lambda,然后应用程序就变成了简化的组合。 - Bartosz Milewski
2个回答

23

显然,>>= 不是表示函数组合的方式。函数组合只需使用.。不过,我认为你读过的任何文章都没有这个意思。

他们的意思是将函数组合“升级”到直接使用“单子函数”,即形如a -> m b 的函数。这种函数的技术术语是Kleisli箭头,确实可以使用<=<>=> 进行组合。(或者,您可以使用Category 实例,然后也可以使用.>>> 进行组合。)

但是,谈论箭头/范畴往往会使初学者感到困惑,就像普通函数的无点定义一样常常令人困惑。幸运的是,Haskell还允许我们以更熟悉的风格表达函数,重点是函数的结果,而不是作为抽象态射的函数本身。这是通过lambda抽象来实现的:而不是

q = h . g . f

你可以写

q = (\x -> (\y -> (\z -> h z) (g y)) (f x))

...当然,首选的样式应该是(这只是Lambda抽象的语法糖!)

q x = let y = f x
          z = g y
      in h z

注意,在lambda表达式中,基本上是将组合替换为了应用:

q = \x -> (\y -> (\z -> h z) $ g y) $ f x

适应Kleisli箭头,这意味着不是

q = h <=< g <=< f

你写

q = \x -> (\y -> (\z -> h z) =<< g y) =<< f x

当然,使用翻转的运算符或语法糖会让代码看起来更加优美:

q x = do y <- f x
         z <- g y
         h z

因此,确实,=<<就像$.一样是<=<。之所以将其称为组合算子仍然是有意义的,是因为除了“应用于值”之外,>>=运算符还执行Kleisli箭头组合的非平凡部分,这是函数组合不需要的:连接单态层。


这样做的原因是Hask是一个笛卡尔闭范畴,特别是好指向范畴。在这样的范畴中,箭头可以被简单参数值应用所得到的所有结果的集合广义地定义。

@adamse指出,let实际上并不是lambda抽象的语法糖。这在递归定义的情况下尤其重要,因为你不能直接使用lambda来编写递归定义。但是在像这里这样的简单情况下,let的行为就像lambda的语法糖一样,就像do符号是lambda和>>=的语法糖一样。(顺便说一句,有一个扩展可以允许递归即使在do符号中……它通过使用固定点组合器绕过lambda限制。)


我认为有关表示函数组合的引用是指将>>==.用于函数作为单子实例。 - Bergi
2
我认为在Haskell中说let绑定是lambda的语法糖是不正确的:https://www.haskell.org/onlinereport/haskell2010/haskellch3.html#x8-440003.12 - ase
@adamse:没错,let确实比lambda更通用。 - leftaroundabout

19

举个例子,比如说:

($)                ::   (a -> b) ->   a ->   b
let g=g in (g  $)  ::                 a ->   b
            g      ::   (a -> b)
                                     _____
Functor f =>                        /     \
(<$>)              ::   (a -> b) -> f a -> f b
let g=g in (g <$>) ::               f a -> f b
            g      ::   (a -> b) 
                       ___________________
Applicative f =>      /             /     \
(<*>)              :: f (a -> b) -> f a -> f b
let h=h in (h <*>) ::               f a -> f b
            h      :: f (a -> b)
                             _____________
Monad m =>                  /.------.     \
(=<<)              :: (a -> m b) -> m a -> m b
let k=k in (k =<<) ::               m a -> m b
            k      :: (a -> m b)

没错,每个形如 (g <$>)(h <*>) 或者 (k =<<) 的操作都是某种函数应用的变体,分别提升到了 Functor、Applicative Functor 或者 Monad 的 "上下文" 中。而 (g $) 则仅仅是一种普通函数的应用。

在 Functor 中,函数对于整体的 thing 中的 f 成分没有影响。它们严格地在内部工作,并不能影响 "wrapping"

在 Applicative 中,函数被包装在一个 f 中,这个包装与参数的包装(作为应用的一部分)相结合,以产生结果的包装。

在 Monad 中,函数本身现在生成被包装的结果,从被包装的参数中提取其参数(作为应用的一部分)。

我们可以将这三个操作看作对函数的一种标记,就像数学家喜欢写成 f' 或者 f^ 或者 f* 一样(在 Eugenio Moggi 的原始作品中(1),确切地说,使用的是推广的函数 (f =<<) 表示为 f*)。

当然,由于提升的函数具有类型 :: f a -> f b,我们可以将它们链接起来,因为类型现在匹配了。提升是允许组合的前提。


(1) "Notions of computation and monads", Eugenio Moggi, July 1991.


因此,Functor 是 "神奇地在" "管道" 内部工作;Applicative 是 "预制的管道从组件中提前构建";而 Monad 则是 "边走边建造管网"。下图是对这些概念的一个图示:

enter image description here


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接