3

假设我有一个 JavaScript 函数

function f(x) {
  return a(b(x), c(x));
}

我如何将其转换为无点函数?通过组合函数?还有关于这方面的更多信息的资源吗?

4

1 回答 1

5

一般来说,当您将函数转换为无点样式时,没有简单的规则可以遵循。要么你必须猜测,要么你可以自动化它。在 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)。这就是函数式编程中所谓的“非柯里化函数”。您还可以想象定义函数,例如

function f(x) {
    return function(y) {
        //body
    }
}

这些函数称为“柯里化函数”。(顺便说一下,如果你想知道这个奇怪的名字,它们是以一个名叫 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);

cpowpow. 它不会同时接受两个论点——而是分别接受它们。在您的具体情况下,这使我们可以从

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));
}

我们现在注意到 this 和 的函数体之间存在一些视觉上的相似性fmap。让我们重写它fmap,它变成

function f(x) {
    return fmap(curry(a), b)(x)(c(x));
}

(看看我是如何到达那里的,想象一下f = curry(a)and g = b。最后一点c(x)没有改变。)

ap

我们的最后一块拼图是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和记忆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);
}

我已将其中一个重命名xy以避免名称冲突。这仍然有点奇怪,但我们可以让它更短一点。毕竟,它是一样的东西

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);

就是这样!

于 2013-06-06T21:08:05.487 回答