为什么函数实例的绑定会为下一次计算提供原始值

Why does bind of the function instance supply the original value to the next computation?

本文关键字:一次 计算 原始 实例 函数 绑定 为什么      更新时间:2023-09-26

作为一个对Haskell只有模糊理解的函数式Javascript开发人员,我真的很难理解像monads这样的Haskell习语。当我查看函数实例的>>=

(>>=)  :: (r -> a) -> (a -> (r -> b)) -> r -> b
instance Monad ((->) r) where
f >>= k = ' r -> k (f r) r
// Javascript:

及其在Javascript中的应用

const bind = f => g => x => g(f(x)) (x);
const inc = x => x + 1;
const f = bind(inc) (x => x <= 5 ? x => x * 2 : x => x * 3);

f(2); // 4
f(5); // 15

一元函数(a -> (r -> b))(或(a -> m b)(提供了一种根据前一个结果选择下一次计算的方法。更一般地说,一元函数及其相应的bind运算符似乎使我们能够定义函数组合在特定计算上下文中的含义。

更令人惊讶的是,monadic 函数没有将前一个计算的结果提供给下一个计算的结果。而是传递原始值。我希望f(2)/f(5)产生6/18,类似于正常的功能组成。此行为是否特定于作为 monad 的函数?我误解了什么?

我认为您的困惑源于使用过于简单的函数。特别是,你写

const inc = x => x + 1;

其类型是一个函数,该函数返回与其输入相同的空间中的值。假设inc正在处理整数。因为它的输入和输出都是整数,如果你有另一个接受整数的函数foo,很容易想象使用 inc输出作为foo输入

不过,现实世界包括更多令人兴奋的功能。考虑函数tree_of_depth,它接受一个整数并创建该深度的字符串树。(我不会尝试实现它,因为我知道的 javascript 还不够多,无法完成令人信服的工作。现在突然之间,很难想象将tree_of_depth的输出作为输入传递给foo,因为foo期待整数,而tree_of_depth正在产生树,对吧?我们唯一可以传递给foo的是输入 tree_of_depth ,因为这是我们唯一的整数,即使在运行 tree_of_depth 之后也是如此。

让我们看看这在绑定的 Haskell 类型签名中是如何体现的:

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

这说明(>>=)需要两个参数,每个参数都函数。第一个函数可以是你喜欢的任何旧类型 - 它可以接受类型r的值并生成类型为 a 的值。特别是,您不必保证ra完全相同。但是一旦你选择了它的类型,那么下一个要(>>=)的函数参数的类型就受到了约束:它必须是两个参数的函数,这两个参数的类型与以前一样ra

现在你可以看到为什么我们必须将相同的类型 r 值传递给这两个函数:第一个函数产生一个a,而不是更新的r,所以我们没有其他类型 r 的值传递给第二个函数!与inc的情况不同,第一个函数碰巧也产生了r,我们可能会产生其他一些非常不同的类型。

这解释了为什么绑定必须按原样实现,但也许不能解释为什么这个 monad 是一个有用的。其他地方有关于这一点的文章。但规范用例是针对配置变量的。假设在程序启动时解析配置文件;然后,对于程序的其余部分,您希望能够通过查看来自该配置的信息来影响各种功能的行为。在所有情况下,使用相同的配置信息都是有意义的 - 它不需要更改。然后,这个 monad 变得很有用:你可以有一个隐式配置值,并且 monad 的绑定操作确保你正在排序的两个函数都可以访问该信息,而不必手动将其传递给这两个函数。

附言你说

更令人惊讶的是,monadic 函数没有将前一个计算的结果提供给下一个计算的结果。

我觉得有点不精确:事实上,在m >>= f中,函数f同时获得m的结果(作为其第一个参数(原始值(作为其第二个参数(。

更一般地说,一元函数及其相应的绑定 运算符似乎让我们能够定义什么函数 组合意味着在特定计算上下文中。

我不确定你所说的"一元函数"是什么意思。Monad(在Haskell中由绑定函数和纯函数组成(让你表达一系列一元动作如何链接在一起((<=<)是组合的monad等价物,相当于Identity monad的(.)(。从这个意义上说,你确实得到了组合,但只是动作的组合(形式a -> m b的函数(。

(这在围绕类型a -> m b函数的 newtype 的 newtype Kleisli中进一步抽象。它的类别实例确实允许您将一元操作的顺序编写为组合。

我希望 f(

2(/f(5( 产生 6/18,类似于正常的函数组合。

然后,你可以只使用正常的函数组合!如果您不需要monad,请不要使用monad。

更令人惊讶的是,

一元函数不提供 上一个计算的结果到下一个计算的结果。相反 传递原始值。 ... 此行为是否特定于 作为单子的功能?

是的,就是这样。monad Monad ((->) r)也被称为"阅读器monad",因为它只从其环境中读取。也就是说,就 monads 而言,您仍然前一个操作的一元结果传递给后续操作 - 但这些结果本身就是函数!

正如 chi 已经提到的,这一行

const f = bind(inc) (x => x <= 5 ? x => x * 2 : x => x * 3);

会更清楚,比如

const f = bind(inc) (x => x <= 5 ? y => y * 2 : y => y * 3);

函数的Monad实例基本上是Reader monad。您有一个值x => x + 1依赖于环境(它为环境增加 1(。

你还有一个函数,根据它的输入,返回一个依赖于环境的值(y => y * 2(或另一个依赖于环境的值(y => y * 3(。

在您的bind中,您仅使用x => x + 1的结果在这两个函数之间进行选择。您不会直接返回上一个结果。但是,如果您返回的常量函数忽略了它们的环境并仅根据先前的结果返回了一个固定值,则可以:

const f = bind(inc) (x => x <= 5 ? _ => x * 2 : _ => x * 3);

(不确定语法(