【问题标题】:Haskell Monad bind operator confusionHaskell Monad 绑定运算符混淆
【发布时间】:2011-12-19 23:23:40
【问题描述】:

好的,所以我不是 Haskell 程序员,但我对 Haskell 背后的许多想法非常感兴趣,并且正在学习它。但我被困在第一方:我似乎无法围绕 Monads,这似乎是相当基本的。我知道关于 SO 有上百万个问题要求解释 Monads,所以我将更具体地说明困扰我的问题:

我阅读了这篇出色的文章 (an introduction in Javascript),并认为我完全理解了 Monads。然后我阅读了关于 Monads 的 Wikipedia 条目,看到了这个:

多态类型(M t)→(t→M u)→(M u)的绑定操作,Haskell用中缀运算符>>=表示。 它的第一个参数是一个单子类型的值,它的第二个参数是一个从第一个参数的基础类型映射到另一个单子类型的函数,其结果是另一个单子类型。

好的,在我引用的那篇文章中,bind 是一个只接受 一个 参数的函数。维基百科说两个。我认为我对 Monads 的理解如下:

  1. Monad 的目的是获取具有不同输入和输出类型的函数并使其可组合。它通过使用单个 monadic 类型包装输入和输出类型来做到这一点。
  2. Monad 由两个相互关联的函数组成:绑定和单元。 Bind 接受一个不可组合的函数 f 并返回一个新函数 g ,该函数接受单子类型作为输入并返回单子类型。 g 是可组合的。 unit 函数接受 f 期望的类型的参数,并将其包装在 monadic 类型中。然后可以将其传递给 g,或像 g 这样的任何函数组合。

但肯定有什么问题,因为我的绑定概念有一个参数:一个函数。但是(根据维基百科)Haskell 的绑定实际上需要 两个 参数!我的错在哪里?

【问题讨论】:

  • 想想如果将参数的顺序翻转为>>= 并将其视为柯里化函数会发生什么。 =<< 接受一个函数 f,并返回一个函数 (f =<<),它接受一元值...
  • Monads 不是基本的。
  • @ephemient:这不是更偏应用而不是柯里化吗?
  • @ivanm:currying 是启用部分应用的转换的名称。他们是一样的。在 Haskell 中,通常以柯里化形式编写多个参数的函数,因此您通常不会注意到这种等价性。
  • Monads 更像方形 14(Real World Haskell,第 14 章 book.realworldhaskell.org/read

标签: haskell monads composition currying function-composition


【解决方案1】:

请允许我推翻你对 Monads 的看法。我真诚地希望您意识到我并不是要无礼;我只是想避免口齿不清。

Monad 的目的是获取具有不同输入和输出类型的函数并使其可组合。它通过使用单个单子类型包装输入和输出类型来做到这一点。

不完全是。当你以“A Monad's purpose”开始一个句子时,你已经走错了路。单子不一定有“目的”。 Monad 只是一种抽象,一种适用于某些类型而不适用于其他类型的分类。 Monad 抽象的目的就是抽象。

Monad 由两个相互关联的函数组成:bind 和 unit。

是的,不是的。 bindunit 的组合足以定义 Monad,但 joinfmapunit 的组合同样足够。事实上,后者正是范畴论中描述 Monad 的典型方式。

Bind 接受一个不可组合的函数 f 并返回一个新函数 g,该函数接受单子类型作为输入并返回单子类型。

同样,不完全是。一元函数f :: a -> m b 是完全可组合的,具有某些类型。我可以用函数g :: m b -> c 后组合它得到g . f :: a -> c,或者我可以用函数h :: c -> a 预先组合它得到f . h :: c -> m b

但是第二部分完全正确:(>>= f) :: m a -> m b。正如其他人所指出的,Haskell 的 bind 函数以相反的顺序接受参数。

g 是可组合的。

嗯,是的。如果是g :: m a -> m b,那么你可以用函数f :: c -> m a预先组合得到g . f :: c -> m b,或者你可以用函数h :: m b -> c后组合得到h . g :: m a -> c。请注意,c 可以采用 m v 的形式,其中 m 是 Monad。我想当你说“可组合”时,你的意思是说“你可以组合任意长的这种形式的函数链”,这是真的。

unit 函数接受 f 所期望的类型的参数,并将其包装在 monadic 类型中。

一种迂回的说法,但是是的,这是正确的。

这个 [将unit 应用到某个值的结果] 然后可以传递给 g,或者像 g 这样的任何函数组合。

再次,是的。虽然调用 unit(或在 Haskell 中为 return)然后将其传递给 (>>= f) 通常不是 Haskell 惯用的做法。

-- instead of
return x >>= f >>= g
-- simply go with
f x >>= g

-- instead of
\x -> return x >>= f >>= g
-- simply go with
f >=> g
-- or
g <=< f

【讨论】:

  • 感谢您帮助我理解。但是如果我有一个函数g :: m a -&gt; m b,我为什么不直接使用f :: a -&gt; b?我不能用类似签名的其他功能组成 g。那么 monad 的其他一些用途是什么让 g 比 f 更有用呢?
  • 好吧,我也许可以想出一个用例来回答我自己的问题:假设我们有一堆函数,每个函数都有一个值(无论是字符串、数字还是其他)作为输入并返回值列表(不一定是相同类型)。如果我们想将一对多函数的结果传递给另一个一对多函数,我们通常必须解包第一个函数返回的列表,将第二个函数应用于每个值,然后连接结果。 monad 可以抽象出所有的样板文件。这是一个有效的例子吗?
  • Ord:这是一个很好的例子,并且正是 Monad 的 List 实例的定义。 return x = [x]; xs &gt;&gt;= f = concatMap f xs。 (警告:附加的,可能令人困惑的思考) concatMap 将函数a -&gt; [b] 映射到a 的列表上,然后将结果列表从[[b]]“展平”到[b]concatMap f xs = concat (map f xs)。这(并非巧合)类似于bind 可以根据fmapjoinany monad 定义的方式:x &gt;&gt;= f = join (fmap f x);您可以看到 join = concatfmap = map 用于列表。
  • “List Monad = 一对多函数”的例子真正帮助我找到了一种一致的方式来思考 Monad,Ord,真是个好选择!
【解决方案2】:

你链接的文章是基于sigfpe的文章,使用了bind的翻转定义:

第一件事是我翻转了bind 的定义并将其写为单词'bind',而通常写为运算符&gt;&gt;=。所以bind f x 通常写成x &gt;&gt;= f

因此,Haskell bind 接受一个包含在 monad 中的值,并返回一个函数,该函数接受一个函数,然后使用提取的值调用它。我可能使用了不精确的术语,所以使用代码可能会更好。

你有:

sine x = (sin x,     "sine was called.")
cube x = (x * x * x, "cube was called.")

现在,翻译你的 JS 绑定(Haskell 会自动进行柯里化,因此调用 bind f 返回一个接受元组的函数,然后模式匹配负责将其解包为 xs,我希望这是可以理解的):

bind f (x, s) = (y, s ++ t)
                where (y, t) = f x

你可以看到它工作:

*Main> :t sine
sine :: Floating t => t -> (t, [Char])
*Main> :t bind sine
bind sine :: Floating t1 => (t1, [Char]) -> (t1, [Char])
*Main> (bind sine . bind cube) (3, "")
(0.956375928404503,"cube was called.sine was called.")

现在,让我们反转bind的参数:

bind' (x, s) f = (y, s ++ t)
                 where (y, t) = f x

您可以清楚地看到它仍在做同样的事情,但语法略有不同:

*Main> bind' (bind' (3, "") cube) sine
(0.956375928404503,"cube was called.sine was called.")

现在,Haskell 有一个语法技巧,允许您将任何函数用作中缀运算符。所以你可以写:

*Main> (3, "") `bind'` cube `bind'` sine
(0.956375928404503,"cube was called.sine was called.")

现在将 bind' 重命名为 &gt;&gt;= ((3, "") &gt;&gt;= cube &gt;&gt;= sine),您就得到了想要的东西。如您所见,使用此定义,您可以有效地摆脱单独的组合运算符。

将新事物翻译回 JavaScript 会产生类似这样的结果(再次注意,我只是颠倒了参数顺序):

var bind = function(tuple) {
    return function(f) {
        var x  = tuple[0],
            s  = tuple[1],
            fx = f(x),
            y  = fx[0],
            t  = fx[1];

        return [y, s + t];
    };
};

// ugly, but it's JS, after all
var f = function(x) { return bind(bind(x)(cube))(sine); }

f([3, ""]); // [0.956375928404503, "cube was called.sine was called."]

希望这会有所帮助,而不是造成更多混乱——关键是这两个绑定定义是等价的,只是调用语法不同。

【讨论】:

  • 我想为了避免JS丑陋,你可以修改Function原型,并使用它“中缀”:g.mBind(f.mBind(mv)),相当于g =&lt;&lt; f =&lt;&lt; mv。更好的方法是在 monadic 值的原型中定义它:mv.bind(f).bind(g),与mv &gt;&gt;= f &gt;&gt;= g 相当。请注意,函数原型显然已经将“绑定”定义为某种东西。
  • 谢谢你,你的例子真的很有帮助!
【解决方案3】:

你没有犯错。这里要理解的关键思想是柯里化——两个参数的 Haskell 函数可以通过两种方式看到。第一个是两个参数的简单函数。例如,如果您有(+),这通常被视为接受两个参数并将它们相加。另一种看待它的方式是作为加法机生产商。 (+) 是一个函数,它接受一个数字,比如 x,并创建一个将添加 x 的函数。

(+) x = \y -> x + y
(+) x y = (\y -> x + y) y = x + y

在处理 monad 时,有时最好考虑一下 =&lt;&lt;,即 &gt;&gt;= 的翻转版本,就像上面提到的那样。有两种方法可以查看:

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

这是一个有两个参数的函数,并且

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

如文章所述,它将输入函数转换为易于组合的版本。这些和我之前解释的(+) 是等价的。

【讨论】:

  • 所以x &gt;&gt;= f(或f =&lt;&lt; x)可以理解为:取f,将其包装成接受一元类型,然后调用包装后的函数,参数为x?跨度>
  • 而当你只给出 >>= 一个参数 (f) 时,bind 基本上部分应用于该参数,返回包装后的 f?
  • 另外,我猜当 bind 被赋予两个参数时,实际上包装 f 并不是真正必要的;从一元类型中提取“核心值”并使用该值调用 f 可能要容易得多。我猜“包装”只是部分应用的一个工件。
  • 是的,是的,而且主要是。大多数 monad,如 Maybe[]Identity,都会解开值并应用它们。但是,您无法打开任何 Nothing[] 的包装。基本上,解包通常是完成的,但也有一些非常小的例外。
  • @Ord "所以x &gt;&gt;= f(或f =&lt;&lt; x)可以理解为:取f,将其包裹,使其接受一元类型,然后调用包裹的带有参数x的函数?”不,不。 m a 类型的 x 是“包装”值,或者最好说“m-type 上下文中的 a-type 值”。 f 只是一个函数,需要一个“简单的”a-type 值,即“包裹在”中,即“in”m-type “context”。所以bind ff 在“内部”上处理该值,否则无法获得。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2018-04-01
  • 2012-09-15
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多