【发布时间】:2013-06-05 03:02:21
【问题描述】:
假设我有一个 JavaScript 函数
function f(x) {
return a(b(x), c(x));
}
如何将其转换为无点函数?通过组合函数?还有资源可以获取更多相关信息吗?
【问题讨论】:
-
applicatives 的案例。不过,它在 JavaScript 中可能看起来不太好。
标签: functional-programming pointfree
假设我有一个 JavaScript 函数
function f(x) {
return a(b(x), c(x));
}
如何将其转换为无点函数?通过组合函数?还有资源可以获取更多相关信息吗?
【问题讨论】:
标签: functional-programming pointfree
一般来说,当您将函数转换为无点样式时,没有一个简单的规则可以遵循。要么你必须猜测,要么你可以自动化它。在 Haskell IRC 频道中,我们有 lambdabot,它非常适合将 Haskell 函数转换为无点样式。我通常只是参考它,然后如果我需要知道它是如何工作的,我就会倒退。
您的特定示例可以使用几个有用的函数来解决。我将在下面向您展示它是如何工作的,但请注意,它可能需要大量玩耍才能理解。如果您了解非常非常基本的 lambda 演算,这也会有所帮助,因为 JavaScript 语法有时会妨碍您。
不管怎样,这里是:
基本上,要正确执行此操作,您需要三个函数:fmap(f, g)、ap(f, g) 和 curry(f)。当你拥有这些时,f(x) 很容易定义为(这在例如 Haskell 中看起来更整洁)
f = ap(fmap(curry(a), b), c);
有趣的地方在于定义这三个函数。
通常当你在 JavaScript 中定义多个参数的函数时,你会像这样定义它们
function f(x, y) {
// body
}
然后您可以通过f(3, 4) 之类的方式调用它们。这就是函数式编程中所谓的“uncurried 函数”。您还可以想象定义函数,如
function f(x) {
return function(y) {
//body
}
}
这些函数称为“curried 函数”。 (顺便说一下,如果你想知道这个奇怪的名字,它们是以一个名叫 Curry 的数学家的名字命名的。)Curried 函数是通过做来调用的
f(3)(4)
但除此之外,这两个函数的行为非常相似。一个区别是,当函数被柯里化时,使用无点样式更容易。我们的curry 函数只需要像第一个一样的非柯里化函数,然后将它变成像第二个一样的柯里化函数。 curry 可以定义为
function curry(f) {
return function(a) {
return function(b) {
return f(a, b);
}
}
}
现在,您可以使用它了。而不是通过pow(3, 4) 来获得 81,您可以这样做
cpow = curry(pow);
cpow(3)(4);
cpow 是 pow 的柯里化版本。它不会同时接受两个论点——而是分别接受它们。在您的具体情况下,这使我们可以从
function f(x) {
return a(b(x), c(x));
}
到
function f(x) {
return curry(a)(b(x))(c(x));
}
这是进步! (虽然我承认它在 JavaScript 中看起来很奇怪......)现在,开始不那么辣的牧场。
谜题的第二部分是fmap(f, g),它将两个函数作为参数并组合它们。我的意思是,
fmap(f, g)(x) == f(g(x))
这很容易定义,我们就让
function fmap(f, g) {
return function(x) {
return f(g(x));
}
}
当您想按顺序执行两件事时,这很有用。假设你想做无用的操作log(exp(x))。你可以用传统的方式做到这一点:
function logexp(x) {
return log(exp(x));
}
你可以这样做
logexp = fmap(log, exp);
这通常称为组合两个函数。要将其与您的示例联系起来,上次我们将其关闭,我们已将其重构为
function f(x) {
return curry(a)(b(x))(c(x));
}
我们现在注意到它与fmap 的函数体之间存在一些视觉上的相似性。让我们用fmap重写它,它变成了
function f(x) {
return fmap(curry(a), b)(x)(c(x));
}
(看看我是如何到达那里的,想象一下f = curry(a) 和g = b。c(x) 的最后一位没有改变。)
我们的最后一个拼图是ap(f, g),它接受两个函数和一个参数,并且做了一件奇怪的事情。我什至不会尝试解释它,所以我只会向您展示它的作用:
ap(f, g)(x) == f(x)(g(x))
请记住,f 实际上只是两个参数的函数,只是我们编写它的方式稍有不同以便能够发挥作用。 ap 在 JavaScript 中定义为
function ap(f, g) {
return function(x) {
return f(x)(g(x));
}
}
所以,把它放在一个更实际的上下文中:假设你想将一个数字提高到它自身的平方根。你可以这样做
function powsqrt(x) {
return pow(x, sqrt(x));
}
或者,凭借您对ap 的新知识并记住第一部分中关于currying 的cpow,您也可以这样做
powsqrt = ap(cpow, sqrt);
这是因为cpow 是pow 的咖喱版本。当ap 的定义扩展时,您可以自己验证这是否正确。
现在,为了将所有这些与您的示例联系在一起,我们需要转向
function f(x) {
return fmap(curry(a), b)(x)(c(x));
}
进入最终的、完全无积分的版本。如果我们查看ap 的定义,我们可以看到我们可以在这里做一些事情来把它变成无点版本!
function f(x) {
return ap(fmap(curry(a), b), c)(x);
}
基本上,理解这一点的最简单方法是现在“展开”对ap 的调用。用函数体替换对ap的调用!那么我们仅仅通过替换得到的是
function f(x) {
return function(y) {
return fmap(curry(a), b)(y)(c(y));
}(x);
}
我已将一个 x 重命名为 y 以避免名称冲突。这仍然有点奇怪,但我们可以让它更短一点。毕竟和
function f(x) {
return fmap(curry(a), b)(x)(c(x));
}
这是我们开始的!我们对ap 的调用是正确的。如果你愿意,你可以进一步展开它,看看在一切都说完之后,我们实际上最终得到了我们开始的东西。我把它留作练习。
不管怎样,你的代码最后一次重构成功了
function f(x) {
return ap(fmap(curry(a), b), c)(x);
}
当然是一样的
f = ap(fmap(curry(a), b), c);
就是这样!
【讨论】: