什么是 Y 组合器?

发布于 2024-07-05 02:38:07 字数 1453 浏览 9 评论 0原文

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(18

尛丟丟 2024-07-12 02:38:07

我认为回答这个问题的最佳方法是选择一种语言,例如 JavaScript:

function factorial(num)
{
    // If the number is less than 0, reject it.
    if (num < 0) {
        return -1;
    }
    // If the number is 0, its factorial is 1.
    else if (num == 0) {
        return 1;
    }
    // Otherwise, call this recursive procedure again.
    else {
        return (num * factorial(num - 1));
    }
}

现在重写它,以便它不使用函数内部的函数名称,但仍然递归调用它。

唯一应该看到函数名称 factorial 的地方是调用站点。

提示:您不能使用函数名称,但可以使用参数名称。

解决问题。 别查了。 一旦你解决了它,你就会明白 y-combinator 解决了什么问题。

I think the best way to answer this is to pick a language, like JavaScript:

function factorial(num)
{
    // If the number is less than 0, reject it.
    if (num < 0) {
        return -1;
    }
    // If the number is 0, its factorial is 1.
    else if (num == 0) {
        return 1;
    }
    // Otherwise, call this recursive procedure again.
    else {
        return (num * factorial(num - 1));
    }
}

Now rewrite it so that it doesn't use the name of the function inside the function, but still calls it recursively.

The only place the function name factorial should be seen is at the call site.

Hint: you can't use names of functions, but you can use names of parameters.

Work the problem. Don't look it up. Once you solve it, you will understand what problem the y-combinator solves.

固执像三岁 2024-07-12 02:38:07

y 组合器实现匿名递归。 因此

function fib( n ){ if( n<=1 ) return n; else return fib(n-1)+fib(n-2) }

而不是您当然可以执行

function ( fib, n ){ if( n<=1 ) return n; else return fib(n-1)+fib(n-2) }

,y 组合器仅适用于按名称调用语言, 的操作。 如果您想在任何正常的按值调用语言中使用它,那么您将需要相关的 z 组合器(y 组合器将发散/无限循环)。

The y-combinator implements anonymous recursion. So instead of

function fib( n ){ if( n<=1 ) return n; else return fib(n-1)+fib(n-2) }

you can do

function ( fib, n ){ if( n<=1 ) return n; else return fib(n-1)+fib(n-2) }

of course, the y-combinator only works in call-by-name languages. If you want to use this in any normal call-by-value language, then you will need the related z-combinator (y-combinator will diverge/infinite-loop).

别靠近我心 2024-07-12 02:38:07

this 运算符可以简化您的生活:

var Y = function(f) {
    return (function(g) {
        return g(g);
    })(function(h) {
        return function() {
            return f.apply(h(h), arguments);
        };
    });
};

然后您可以避免额外的函数:

var fac = Y(function(n) {
    return n == 0 ? 1 : n * this(n - 1);
});

最后,您调用 fac(5)

The this-operator can simplify your life:

var Y = function(f) {
    return (function(g) {
        return g(g);
    })(function(h) {
        return function() {
            return f.apply(h(h), arguments);
        };
    });
};

Then you avoid the extra function:

var fac = Y(function(n) {
    return n == 0 ? 1 : n * this(n - 1);
});

Finally, you call fac(5).

筑梦 2024-07-12 02:38:07

定点组合器(或定点运算符)是计算其他函数的定点的高阶函数。 此操作与编程语言理论相关,因为它允许以重写规则的形式实现递归,而无需语言运行时引擎的显式支持。 (src 维基百科)

A fixed point combinator (or fixed-point operator) is a higher-order function that computes a fixed point of other functions. This operation is relevant in programming language theory because it allows the implementation of recursion in the form of a rewrite rule, without explicit support from the language's runtime engine. (src Wikipedia)

仅此而已 2024-07-12 02:38:07

以下是原始问题的答案,编译自文章(完全值得一读)尼古拉斯·曼库索的回答< /a>,以及其他答案:

什么是 Y 组合器?

Y 组合器是一个“函数”(或高阶函数 - 对其他函数进行操作的函数),它采用单个参数(非递归函数),并返回该函数的版本递归的。


有点递归=),但是更深入的定义:

组合器——只是一个没有自由变量的 lambda 表达式。

自由变量——是不是绑定变量的变量。

绑定变量 — 包含在 lambda 表达式主体内的变量,该表达式将该变量名称作为其参数之一。

另一种思考方式是,组合器是这样一个 lambda 表达式,在其中,您可以在找到组合器的任何地方用其定义替换组合器的名称,并且一切仍然有效(如果组合器在 lambda 体内包含对其自身的引用)。

Y-组合器是一个定点组合器。

函数的不动点是函数域中由函数映射到其自身的元素。

也就是说,如果f(c) = cc是函数f(x)的不动点

这意味着f(f(...f(c)...)) = fn(c) = c

组合器如何工作?

下面的示例假设强+动态输入:

惰性(正序)Y 组合器:

此定义适用于具有惰性(也称为延迟、按需调用)求值的语言 - 求值策略延迟表达式的求值,直到需要其值为止。

Y = λf.(λx.f(x x)) (λx.f(x x)) = λf.(λx.(x x)) (λx.f(x x))

这意味着,对于给定的函数f(非递归函数),可以先通过计算λx.f(xx)得到对应的递归函数,然后应用这个 lambda 表达式本身。

严格(应用顺序)Y 组合器:

此定义适用于具有严格(也:急切、贪婪)求值的语言 - 求值策略,其中表达式一旦绑定到变量就对其进行求值。

Y = λf.(λx.f(λy.((x x) y))) (λx.f(λy.((x x) y))) = λf.(λx.(x x)) (λx.f(λy.((x x) y)))

它与惰性求值相同从本质上讲,它只是有一个额外的 λ 包装器来延迟 lambda 的主体评估。 我问了另一个问题,有点与此主题相关。

它们有什么用处?

被盗借自Chris Ammerman的回答:Y-combinator概括递归,抽象其实现,从而将其与相关函数的实际工作分开。

尽管 Y-combinator 有一些实际应用,但它主要是一个理论概念,对其的理解将扩大您的整体视野,并可能提高您的分析和开发技能。

它们在过程语言中有用吗?

正如 Mike Vanier 所说的所述可以定义一个 Y 组合器在许多静态类型语言中,但是(至少在我见过的示例中)这样的定义通常需要一些不明显的类型黑客,因为 Y 组合器本身没有简单的静态类型。 这超出了本文的范围,因此我不会进一步提及

正如 Chris Ammerman 提到的:大多数过程语言具有静态类型。

所以回答这个问题——不是真的。

Here are answers to the original questions, compiled from the article (which is TOTALY worth reading) mentioned in the answer by Nicholas Mancuso, as well as other answers:

What is a Y-combinator?

An Y-combinator is a "functional" (or a higher-order function — a function that operates on other functions) that takes a single argument, which is a function that isn't recursive, and returns a version of the function which is recursive.


Somewhat recursive =), but more in-depth definition:

A combinator — is just a lambda expression with no free variables.

Free variable — is a variable that is not a bound variable.

Bound variable — variable which is contained inside the body of a lambda expression that has that variable name as one of its arguments.

Another way to think about this is that combinator is such a lambda expression, in which you are able to replace the name of a combinator with its definition everywhere it is found and have everything still work (you will get into an infinite loop if combinator would contain reference to itself, inside the lambda body).

Y-combinator is a fixed-point combinator.

Fixed point of a function is an element of the function's domain that is mapped to itself by the function.

That is to say, c is a fixed point of the function f(x) if f(c) = c

This means f(f(...f(c)...)) = fn(c) = c

How do combinators work?

Examples below assume strong + dynamic typing:

Lazy (normal-order) Y-combinator:

This definition applies to languages with lazy (also: deferred, call-by-need) evaluation — evaluation strategy which delays the evaluation of an expression until its value is needed.

Y = λf.(λx.f(x x)) (λx.f(x x)) = λf.(λx.(x x)) (λx.f(x x))

What this means is that, for a given function f (which is a non-recursive function), the corresponding recursive function can be obtained first by computing λx.f(x x), and then applying this lambda expression to itself.

Strict (applicative-order) Y-combinator:

This definition applies to languages with strict (also: eager, greedy) evaluation — evaluation strategy in which an expression is evaluated as soon as it is bound to a variable.

Y = λf.(λx.f(λy.((x x) y))) (λx.f(λy.((x x) y))) = λf.(λx.(x x)) (λx.f(λy.((x x) y)))

It is same as lazy one in it's nature, it just has an extra λ wrappers to delay the lambda's body evaluation. I've asked another question, somewhat related to this topic.

What are they good for?

Stolen borrowed from answer by Chris Ammerman: Y-combinator generalizes recursion, abstracting its implementation, and thereby separating it from the actual work of the function in question.

Even though, Y-combinator has some practical applications, it is mainly a theoretical concept, understanding of which will expand your overall vision and will, likely, increase your analytical and developer skills.

Are they useful in procedural languages?

As stated by Mike Vanier: it is possible to define a Y combinator in many statically typed languages, but (at least in the examples I've seen) such definitions usually require some non-obvious type hackery, because the Y combinator itself doesn't have a straightforward static type. That's beyond the scope of this article, so I won't mention it further

And as mentioned by Chris Ammerman: most procedural languages has static-typing.

So answer to this one — not really.

咆哮 2024-07-12 02:38:07

作为组合器的新手,我发现 Mike Vanier 的文章(感谢 Nicholas Mancuso)非常有帮助。 我想写一个总结,除了记录我的理解之外,如果能对其他人有帮助我会很高兴。

蹩脚少蹩脚

以阶乘为例,我们使用以下almost-factorial函数来计算数字x<的阶乘/code>:

def almost-factorial f x = if iszero x
                           then 1
                           else * x (f (- x 1))

在上面的伪代码中,almost-factorial 接受函数 f 和数字 x (almost-factorial< /code> 是柯里化的,因此它可以被视为接受函数 f 并返回一个 1 元函数)。

almost-factorial 计算 x 的阶乘时,它将 x - 1 的阶乘计算委托给函数 f并将该结果与 x 相加(在本例中,它将 (x - 1) 的结果与 x 相乘)。

它可以被视为almost-factorial采用阶乘函数的蹩脚版本(只能计算到数字x - 1)并返回阶乘的不太蹩脚版本(计算到数字x)。 就像这种形式一样:

almost-factorial crappy-f = less-crappy-f

如果我们重复地将阶乘的less-crappy版本传递给almost-factorial,我们最终将得到我们想要的阶乘函数f。 可以将其视为:

almost-factorial f = f

定点

事实上,几乎阶乘 f = f 意味着 f 是函数 < 的定点代码>几乎阶乘。

这是查看上述函数之间关系的一种非常有趣的方式,这对我来说是一个顿悟的时刻。 (如果您还没有阅读过,请阅读 Mike 关于定点的文章)

三个函数

概括来说,我们有一个非递归函数 fn (就像我们的近似阶乘),我们有它的定点函数fr(就像我们的f),那么Y所做的就是当你给出Y时> fn, Y 返回fn 的定点函数。

总而言之(通过假设 fr 仅采用一个参数来简化;x 退化为 x - 1x - 2...在递归中):

  • 我们将核心计算定义为fndef fn fr x = ...accumulate x结果来自 (fr (- x 1)),这是几乎有用函数 - 尽管我们不能使用 fn直接在 x 上,很快就会有用。 这个非递归fn使用函数fr来计算其结果
  • fn fr = frfr fn的不动点,fr有用函数,我们可以使用< code>fr 在 x 上获取结果
  • Y fn = frY 返回修复结果- 函数的点,Y 将我们的几乎有用函数fn变成有用 fr

推导Y(不包括)

我将跳过Y的推导并去理解Y。 Mike Vainer 的帖子有很多细节。

Y Y 的形式

定义为(以 lambda calculus 格式):

Y f = λs.(f (s s)) λs.(f (s s))

如果我们将变量 s 替换为函数左边,我们得到

Y f = λs.(f (s s)) λs.(f (s s))
=> f (λs.(f (s s)) λs.(f (s s)))
=> f (Y f)

所以,(Y f) 的结果确实是 f 的不动点。

为什么 (Y f) 有效?

根据 f 的签名,(Y f) 可以是任意数量的函数,为了简化,我们假设 (Y f) 只需要一个参数,就像我们的阶乘函数一样。

def fn fr x = accumulate x (fr (- x 1))

由于fn fr = fr,我们继续

=> accumulate x (fn fr (- x 1))
=> accumulate x (accumulate (- x 1) (fr (- x 2)))
=> accumulate x (accumulate (- x 1) (accumulate (- x 2) ... (fn fr 1)))

递归计算,当最里面的(fn fr 1)是基本情况并且fn不存在时终止计算中不要使用fr

再次查看 Y

fr = Y fn = λs.(fn (s s)) λs.(fn (s s))
=> fn (λs.(fn (s s)) λs.(fn (s s)))

所以

fr x = Y fn x = fn (λs.(fn (s s)) λs.(fn (s s))) x

对我来说,此设置的神奇部分是:

  • fnfr 相互依赖:fr '包裹' fn 内部,每次使用 fr 计算 x 时,它都会“生成”(“提升”?)一个 fn 并将计算委托给该 fn(传入 frx 本身); 另一方面,fn 依赖于 fr 并使用 fr 计算较小问题 x-1 的结果。
  • fr 用于定义 fn 时(当 fn 在其操作中使用 fr 时),真正的 < code>fr 尚未定义。
  • fn 定义了真正的业务逻辑。 在fn的基础上,Y创建了fr——一个特定形式的辅助函数——以方便计算fn递归方式。

它帮助我现在以这种方式理解Y,希望它有所帮助。

顺便说一句,我还发现这本书通过 Lambda Calculus 进行函数式编程简介非常好,我只读了一部分,事实上我无法理解书中的 Y,这导致我写了这篇文章。

As a newbie to combinators, I found Mike Vanier's article (thanks Nicholas Mancuso) to be really helpful. I would like to write a summary, besides documenting my understanding, if it could be of help to some others I would be very glad.

From Crappy to Less Crappy

Using factorial as an example, we use the following almost-factorial function to calculate factorial of number x:

def almost-factorial f x = if iszero x
                           then 1
                           else * x (f (- x 1))

In the pseudo-code above, almost-factorial takes in function f and number x (almost-factorial is curried, so it can be seen as taking in function f and returning a 1-arity function).

When almost-factorial calculates factorial for x, it delegates the calculation of factorial for x - 1 to function f and accumulates that result with x (in this case, it multiplies the result of (x - 1) with x).

It can be seen as almost-factorial takes in a crappy version of factorial function (which can only calculate till number x - 1) and returns a less-crappy version of factorial (which calculates till number x). As in this form:

almost-factorial crappy-f = less-crappy-f

If we repeatedly pass the less-crappy version of factorial to almost-factorial, we will eventually get our desired factorial function f. Where it can be considered as:

almost-factorial f = f

Fix-point

The fact that almost-factorial f = f means f is the fix-point of function almost-factorial.

This was a really interesting way of seeing the relationships of the functions above and it was an aha moment for me. (please read Mike's post on fix-point if you haven't)

Three functions

To generalize, we have a non-recursive function fn (like our almost-factorial), we have its fix-point function fr (like our f), then what Y does is when you give Y fn, Y returns the fix-point function of fn.

So in summary (simplified by assuming fr takes only one parameter; x degenerates to x - 1, x - 2... in recursion):

  • We define the core calculations as fn: def fn fr x = ...accumulate x with result from (fr (- x 1)), this is the almost-useful function - although we cannot use fn directly on x, it will be useful very soon. This non-recursive fn uses a function fr to calculate its result
  • fn fr = fr, fr is the fix-point of fn, fr is the useful funciton, we can use fr on x to get our result
  • Y fn = fr, Y returns the fix-point of a function, Y turns our almost-useful function fn into useful fr

Deriving Y (not included)

I will skip the derivation of Y and go to understanding Y. Mike Vainer's post has a lot of details.

The form of Y

Y is defined as (in lambda calculus format):

Y f = λs.(f (s s)) λs.(f (s s))

If we replace the variable s in the left of the functions, we get

Y f = λs.(f (s s)) λs.(f (s s))
=> f (λs.(f (s s)) λs.(f (s s)))
=> f (Y f)

So indeed, the result of (Y f) is the fix-point of f.

Why does (Y f) work?

Depending the signature of f, (Y f) can be a function of any arity, to simplify, let's assume (Y f) only takes one parameter, like our factorial function.

def fn fr x = accumulate x (fr (- x 1))

since fn fr = fr, we continue

=> accumulate x (fn fr (- x 1))
=> accumulate x (accumulate (- x 1) (fr (- x 2)))
=> accumulate x (accumulate (- x 1) (accumulate (- x 2) ... (fn fr 1)))

the recursive calculation terminates when the inner-most (fn fr 1) is the base case and fn doesn't use fr in the calculation.

Looking at Y again:

fr = Y fn = λs.(fn (s s)) λs.(fn (s s))
=> fn (λs.(fn (s s)) λs.(fn (s s)))

So

fr x = Y fn x = fn (λs.(fn (s s)) λs.(fn (s s))) x

To me, the magical parts of this setup are:

  • fn and fr interdepend on each other: fr 'wraps' fn inside, every time fr is used to calculate x, it 'spawns' ('lifts'?) an fn and delegates the calculation to that fn (passing in itself fr and x); on the other hand, fn depends on fr and uses fr to calculate result of a smaller problem x-1.
  • At the time fr is used to define fn (when fn uses fr in its operations), the real fr is not yet defined.
  • It's fn which defines the real business logic. Based on fn, Y creates fr - a helper function in a specific form - to facilitate the calculation for fn in a recursive manner.

It helped me understanding Y this way at the moment, hope it helps.

BTW, I also found the book An Introduction to Functional Programming Through Lambda Calculus very good, I'm only part through it and the fact that I couldn't get my head around Y in the book led me to this post.

听,心雨的声音 2024-07-12 02:38:07

Y 组合器是磁通电容器的别称。

A Y-Combinator is another name for a flux capacitor.

爱给你人给你 2024-07-12 02:38:07

我在 Clojure 和 Scheme 中写了一份 Y-Combinator 的“白痴指南”,以帮助自己掌握它。 他们受到“小阴谋家”中的材料的影响

:计划中:
https://gist.github.com/z5h/238891

或 Clojure:
https://gist.github.com/z5h/5102747

两个教程都是代码,中间散布着注释并且应该被切割& 可粘贴到您最喜欢的编辑器中。

I have written a sort of "idiots guide" to the Y-Combinator in both Clojure and Scheme in order to help myself come to grips with it. They are influenced by material in "The Little Schemer"

In Scheme:
https://gist.github.com/z5h/238891

or Clojure:
https://gist.github.com/z5h/5102747

Both tutorials are code interspersed with comments and should be cut & pastable into your favourite editor.

七月上 2024-07-12 02:38:07

其他答案对此提供了非常简洁的答案,但没有一个重要的事实:您不需要以这种复杂的方式用任何实用语言实现定点组合器,这样做没有任何实际目的(除了“看,我知道 Y 组合器是什么”)是”)。 这是重要的理论概念,但实用价值不大。

Other answers provide pretty concise answer to this, without one important fact: You don't need to implement fixed point combinator in any practical language in this convoluted way and doing so serves no practical purpose (except "look, I know what Y-combinator is"). It's important theoretical concept, but of little practical value.

时光礼记 2024-07-12 02:38:07

下面是 Y-Combinator 和 Factorial 函数的 JavaScript 实现(来自 Douglas Crockford 的文章,可从以下位置获取:http: //javascript.crockford.com/little.html)。

function Y(le) {
    return (function (f) {
        return f(f);
    }(function (f) {
        return le(function (x) {
            return f(f)(x);
        });
    }));
}

var factorial = Y(function (fac) {
    return function (n) {
        return n <= 2 ? n : n * fac(n - 1);
    };
});

var number120 = factorial(5);

Here is a JavaScript implementation of the Y-Combinator and the Factorial function (from Douglas Crockford's article, available at: http://javascript.crockford.com/little.html).

function Y(le) {
    return (function (f) {
        return f(f);
    }(function (f) {
        return le(function (x) {
            return f(f)(x);
        });
    }));
}

var factorial = Y(function (fac) {
    return function (n) {
        return n <= 2 ? n : n * fac(n - 1);
    };
});

var number120 = factorial(5);
下壹個目標 2024-07-12 02:38:07

匿名递归

定点组合器是一个高阶函数 fix,根据定义满足等价

forall f.  fix f  =  f (fix f)

fix f 表示固定点的解 x -点方程

               x  =  f x

自然数的阶乘可以通过使用fix来证明

fact 0 = 1
fact n = n * fact (n - 1)

,可以在没有匿名自指的情况下导出一般/μ-递归函数的任意构造性证明。

fact n = (fix fact') n

其中

fact' rec n = if n == 0
                then 1
                else n * rec (n - 1)

   fact 3
=  (fix fact') 3
=  fact' (fix fact') 3
=  if 3 == 0 then 1 else 3 * (fix fact') (3 - 1)
=  3 * (fix fact') 2
=  3 * fact' (fix fact') 2
=  3 * if 2 == 0 then 1 else 2 * (fix fact') (2 - 1)
=  3 * 2 * (fix fact') 1
=  3 * 2 * fact' (fix fact') 1
=  3 * 2 * if 1 == 0 then 1 else 1 * (fix fact') (1 - 1)
=  3 * 2 * 1 * (fix fact') 0
=  3 * 2 * 1 * fact' (fix fact') 0
=  3 * 2 * 1 * if 0 == 0 then 1 else 0 * (fix fact') (0 - 1)
=  3 * 2 * 1 * 1
=  6

这个正式证明

fact 3  =  6

有条不紊地使用定点组合子等价重写

fix fact'  ->  fact' (fix fact')

Lambda演算

无类型 lambda 演算形式主义包含上下文无关语法,

E ::= v        Variable
   |  λ v. E   Abstraction
   |  E E      Application

其中v 变量范围,以及 betaeta 缩减 规则

(λ x. B) E  ->  B[x := E]                                 Beta
  λ x. E x  ->  E          if x doesn’t occur free in E   Eta

Beta 缩减替换抽象中变量 x 的所有自由出现(“函数”)主体 B 由表达式(“参数”)E 组成。 Eta 减少消除了多余的抽象。 有时在形式主义中会省略它。 不适用归约规则的不可约表达式采用正规规范形式

λ x y. E

的简写

λ x. λ y. E

是(抽象多重性)

E F G

,是

(E F) G

(应用程序左关联性)的简写,

λ x. x

并且

λ y. y

是 alpha 等价的。

抽象和应用是 lambda 演算的两个唯一的“语言原语”,但它们允许对任意复杂的数据和操作进行编码。

丘奇数字是自然数的编码,类似于皮亚诺公理自然数。

   0  =  λ f x. x                 No application
   1  =  λ f x. f x               One application
   2  =  λ f x. f (f x)           Twofold
   3  =  λ f x. f (f (f x))       Threefold
    . . .

SUCC  =  λ n f x. f (n f x)       Successor
 ADD  =  λ n m f x. n f (m f x)   Addition
MULT  =  λ n m f x. n (m f) x     Multiplication
    . . .

形式证明

1 + 2  =  3

使用 beta 约简重写规则的

   ADD                      1            2
=  (λ n m f x. n f (m f x)) (λ g y. g y) (λ h z. h (h z))
=  (λ m f x. (λ g y. g y) f (m f x)) (λ h z. h (h z))
=  (λ m f x. (λ y. f y) (m f x)) (λ h z. h (h z))
=  (λ m f x. f (m f x)) (λ h z. h (h z))
=  λ f x. f ((λ h z. h (h z)) f x)
=  λ f x. f ((λ z. f (f z)) x)
=  λ f x. f (f (f x))                                       Normal form
=  3

:组合器

在 lambda 演算中,组合器是不包含自由变量的抽象。 组合器

λ x. x

最简单的是:I,与恒等函数同构的恒等

id x = x

此类组合器是像 SKI 系统这样的组合器演算的原始算子。

S  =  λ x y z. x z (y z)
K  =  λ x y. x
I  =  λ x. x

Beta 减少并不是强烈标准化; 并非所有可约表达式“redexes”都会在 beta 约简下收敛到正常形式。 一个简单的例子是 omega ω 组合器

λ x. x x

对其自身的发散应用:

   (λ x. x x) (λ y. y y)
=  (λ y. y y) (λ y. y y)
. . .
=  _|_                     Bottom

优先考虑减少最左边的子表达式(“头”)。 应用顺序在替换之前规范参数,正常顺序则不然。 这两种策略类似于急切求值(例如 C)和惰性求值(例如 Haskell)。

   K          (I a)        (ω ω)
=  (λ k l. k) ((λ i. i) a) ((λ x. x x) (λ y. y y))

在急切的应用阶 beta 约简下发散

=  (λ k l. k) a ((λ x. x x) (λ y. y y))
=  (λ l. a) ((λ x. x x) (λ y. y y))
=  (λ l. a) ((λ y. y y) (λ y. y y))
. . .
=  _|_

由于在严格语义中,

forall f.  f _|_  =  _|_

,但在惰性正序 beta 约简下收敛

=  (λ l. ((λ i. i) a)) ((λ x. x x) (λ y. y y))
=  (λ l. a) ((λ x. x x) (λ y. y y))
=  a

如果一个表达式具有范式,则正常阶 beta 约简将会找到它。

Y

Y 定点组合器的基本属性

λ f. (λ x. f (x x)) (λ x. f (x x))

由下式给出

   Y g
=  (λ f. (λ x. f (x x)) (λ x. f (x x))) g
=  (λ x. g (x x)) (λ x. g (x x))           =  Y g
=  g ((λ x. g (x x)) (λ x. g (x x)))       =  g (Y g)
=  g (g ((λ x. g (x x)) (λ x. g (x x))))   =  g (g (Y g))
. . .                                      . . .

:等价性

Y g  =  g (Y g)

同构于

fix f  =  f (fix f)

无类型 lambda 演算可以对一般/μ-递归函数上的任意构造性证明进行编码。

 FACT  =  λ n. Y FACT' n
FACT'  =  λ rec n. if n == 0 then 1 else n * rec (n - 1)

   FACT 3
=  (λ n. Y FACT' n) 3
=  Y FACT' 3
=  FACT' (Y FACT') 3
=  if 3 == 0 then 1 else 3 * (Y FACT') (3 - 1)
=  3 * (Y FACT') (3 - 1)
=  3 * FACT' (Y FACT') 2
=  3 * if 2 == 0 then 1 else 2 * (Y FACT') (2 - 1)
=  3 * 2 * (Y FACT') 1
=  3 * 2 * FACT' (Y FACT') 1
=  3 * 2 * if 1 == 0 then 1 else 1 * (Y FACT') (1 - 1)
=  3 * 2 * 1 * (Y FACT') 0
=  3 * 2 * 1 * FACT' (Y FACT') 0
=  3 * 2 * 1 * if 0 == 0 then 1 else 0 * (Y FACT') (0 - 1)
=  3 * 2 * 1 * 1
=  6

(乘法延迟,汇合)

对于丘奇无类型 lambda 演算,已证明除了 Y 之外还存在递归可枚举的定点组合器。

 X  =  λ f. (λ x. x x) (λ x. f (x x))
Y'  =  (λ x y. x y x) (λ y x. y (x y x))
 Z  =  λ f. (λ x. f (λ v. x x v)) (λ x. f (λ v. x x v))
 Θ  =  (λ x y. y (x x y)) (λ x y. y (x x y))
  . . .

正态阶 beta 约简使得未扩展的无类型 lambda 演算成为图灵完备的重写系统。

在 Haskell 中,定点组合器可以优雅地实现,

fix :: forall t. (t -> t) -> t
fix f = f (fix f)

在计算所有子表达式之前,Haskell 的惰性标准化为有限。

primes :: Integral t => [t]
primes = sieve [2 ..]
   where
      sieve = fix (\ rec (p : ns) ->
                     p : rec [n | n <- ns
                                , n `rem` p /= 0])

Anonymous recursion

A fixed-point combinator is a higher-order function fix that by definition satisfies the equivalence

forall f.  fix f  =  f (fix f)

fix f represents a solution x to the fixed-point equation

               x  =  f x

The factorial of a natural number can be proved by

fact 0 = 1
fact n = n * fact (n - 1)

Using fix, arbitrary constructive proofs over general/μ-recursive functions can be derived without nonymous self-referentiality.

fact n = (fix fact') n

where

fact' rec n = if n == 0
                then 1
                else n * rec (n - 1)

such that

   fact 3
=  (fix fact') 3
=  fact' (fix fact') 3
=  if 3 == 0 then 1 else 3 * (fix fact') (3 - 1)
=  3 * (fix fact') 2
=  3 * fact' (fix fact') 2
=  3 * if 2 == 0 then 1 else 2 * (fix fact') (2 - 1)
=  3 * 2 * (fix fact') 1
=  3 * 2 * fact' (fix fact') 1
=  3 * 2 * if 1 == 0 then 1 else 1 * (fix fact') (1 - 1)
=  3 * 2 * 1 * (fix fact') 0
=  3 * 2 * 1 * fact' (fix fact') 0
=  3 * 2 * 1 * if 0 == 0 then 1 else 0 * (fix fact') (0 - 1)
=  3 * 2 * 1 * 1
=  6

This formal proof that

fact 3  =  6

methodically uses the fixed-point combinator equivalence for rewrites

fix fact'  ->  fact' (fix fact')

Lambda calculus

The untyped lambda calculus formalism consists in a context-free grammar

E ::= v        Variable
   |  λ v. E   Abstraction
   |  E E      Application

where v ranges over variables, together with the beta and eta reduction rules

(λ x. B) E  ->  B[x := E]                                 Beta
  λ x. E x  ->  E          if x doesn’t occur free in E   Eta

Beta reduction substitutes all free occurrences of the variable x in the abstraction (“function”) body B by the expression (“argument”) E. Eta reduction eliminates redundant abstraction. It is sometimes omitted from the formalism. An irreducible expression, to which no reduction rule applies, is in normal or canonical form.

λ x y. E

is shorthand for

λ x. λ y. E

(abstraction multiarity),

E F G

is shorthand for

(E F) G

(application left-associativity),

λ x. x

and

λ y. y

are alpha-equivalent.

Abstraction and application are the two only “language primitives” of the lambda calculus, but they allow encoding of arbitrarily complex data and operations.

The Church numerals are an encoding of the natural numbers similar to the Peano-axiomatic naturals.

   0  =  λ f x. x                 No application
   1  =  λ f x. f x               One application
   2  =  λ f x. f (f x)           Twofold
   3  =  λ f x. f (f (f x))       Threefold
    . . .

SUCC  =  λ n f x. f (n f x)       Successor
 ADD  =  λ n m f x. n f (m f x)   Addition
MULT  =  λ n m f x. n (m f) x     Multiplication
    . . .

A formal proof that

1 + 2  =  3

using the rewrite rule of beta reduction:

   ADD                      1            2
=  (λ n m f x. n f (m f x)) (λ g y. g y) (λ h z. h (h z))
=  (λ m f x. (λ g y. g y) f (m f x)) (λ h z. h (h z))
=  (λ m f x. (λ y. f y) (m f x)) (λ h z. h (h z))
=  (λ m f x. f (m f x)) (λ h z. h (h z))
=  λ f x. f ((λ h z. h (h z)) f x)
=  λ f x. f ((λ z. f (f z)) x)
=  λ f x. f (f (f x))                                       Normal form
=  3

Combinators

In lambda calculus, combinators are abstractions that contain no free variables. Most simply: I, the identity combinator

λ x. x

isomorphic to the identity function

id x = x

Such combinators are the primitive operators of combinator calculi like the SKI system.

S  =  λ x y z. x z (y z)
K  =  λ x y. x
I  =  λ x. x

Beta reduction is not strongly normalizing; not all reducible expressions, “redexes”, converge to normal form under beta reduction. A simple example is divergent application of the omega ω combinator

λ x. x x

to itself:

   (λ x. x x) (λ y. y y)
=  (λ y. y y) (λ y. y y)
. . .
=  _|_                     Bottom

Reduction of leftmost subexpressions (“heads”) is prioritized. Applicative order normalizes arguments before substitution, normal order does not. The two strategies are analogous to eager evaluation, e.g. C, and lazy evaluation, e.g. Haskell.

   K          (I a)        (ω ω)
=  (λ k l. k) ((λ i. i) a) ((λ x. x x) (λ y. y y))

diverges under eager applicative-order beta reduction

=  (λ k l. k) a ((λ x. x x) (λ y. y y))
=  (λ l. a) ((λ x. x x) (λ y. y y))
=  (λ l. a) ((λ y. y y) (λ y. y y))
. . .
=  _|_

since in strict semantics

forall f.  f _|_  =  _|_

but converges under lazy normal-order beta reduction

=  (λ l. ((λ i. i) a)) ((λ x. x x) (λ y. y y))
=  (λ l. a) ((λ x. x x) (λ y. y y))
=  a

If an expression has a normal form, normal-order beta reduction will find it.

Y

The essential property of the Y fixed-point combinator

λ f. (λ x. f (x x)) (λ x. f (x x))

is given by

   Y g
=  (λ f. (λ x. f (x x)) (λ x. f (x x))) g
=  (λ x. g (x x)) (λ x. g (x x))           =  Y g
=  g ((λ x. g (x x)) (λ x. g (x x)))       =  g (Y g)
=  g (g ((λ x. g (x x)) (λ x. g (x x))))   =  g (g (Y g))
. . .                                      . . .

The equivalence

Y g  =  g (Y g)

is isomorphic to

fix f  =  f (fix f)

The untyped lambda calculus can encode arbitrary constructive proofs over general/μ-recursive functions.

 FACT  =  λ n. Y FACT' n
FACT'  =  λ rec n. if n == 0 then 1 else n * rec (n - 1)

   FACT 3
=  (λ n. Y FACT' n) 3
=  Y FACT' 3
=  FACT' (Y FACT') 3
=  if 3 == 0 then 1 else 3 * (Y FACT') (3 - 1)
=  3 * (Y FACT') (3 - 1)
=  3 * FACT' (Y FACT') 2
=  3 * if 2 == 0 then 1 else 2 * (Y FACT') (2 - 1)
=  3 * 2 * (Y FACT') 1
=  3 * 2 * FACT' (Y FACT') 1
=  3 * 2 * if 1 == 0 then 1 else 1 * (Y FACT') (1 - 1)
=  3 * 2 * 1 * (Y FACT') 0
=  3 * 2 * 1 * FACT' (Y FACT') 0
=  3 * 2 * 1 * if 0 == 0 then 1 else 0 * (Y FACT') (0 - 1)
=  3 * 2 * 1 * 1
=  6

(Multiplication delayed, confluence)

For Churchian untyped lambda calculus, there has been shown to exist a recursively enumerable infinity of fixed-point combinators besides Y.

 X  =  λ f. (λ x. x x) (λ x. f (x x))
Y'  =  (λ x y. x y x) (λ y x. y (x y x))
 Z  =  λ f. (λ x. f (λ v. x x v)) (λ x. f (λ v. x x v))
 Θ  =  (λ x y. y (x x y)) (λ x y. y (x x y))
  . . .

Normal-order beta reduction makes the unextended untyped lambda calculus a Turing-complete rewrite system.

In Haskell, the fixed-point combinator can be elegantly implemented

fix :: forall t. (t -> t) -> t
fix f = f (fix f)

Haskell’s laziness normalizes to a finity before all subexpressions have been evaluated.

primes :: Integral t => [t]
primes = sieve [2 ..]
   where
      sieve = fix (\ rec (p : ns) ->
                     p : rec [n | n <- ns
                                , n `rem` p /= 0])

浮生未歇 2024-07-12 02:38:07

上面的大多数答案描述了 Y 组合器是什么,但没有描述它的用途

定点组合器用于表明lambda 演算图灵完备。 这是计算理论中非常重要的结果,为函数式编程提供了理论基础。

研究定点组合器也帮助我真正理解了函数式编程。 但我从未在实际编程中发现它们有任何用处。

Most of the answers above describe what the Y-combinator is but not what it is for.

Fixed point combinators are used to show that lambda calculus is turing complete. This is a very important result in the theory of computation and provides a theoretical foundation for functional programming.

Studying fixed point combinators has also helped me really understand functional programming. I have never found any use for them in actual programming though.

逆流 2024-07-12 02:38:07

对于没有深入接触过函数式编程,并且不想现在开始,但有一点好奇的程序员:

Y 组合子是一个公式,它可以让你在函数可以实现递归的情况下实现递归。没有名称,但可以作为参数传递、用作返回值以及在其他函数中定义。

它的工作原理是将函数作为参数传递给自身,以便它可以调用自身。

它是 lambda 演算的一部分,它实际上是数学,但实际上是一种编程语言,并且是计算机科学,尤其是函数式编程的基础。

Y 组合器的日常实用价值是有限的,因为编程语言倾向于让您命名函数。

如果您需要在警察阵容中识别它,它看起来像这样:

Y = λf.(λx.f (xx)) (λx.f (xx))

由于重复的(λx.f (xx)),您通常可以发现它。

λ 符号是希腊字母 lambda,这就是 lambda 演算的名字,并且有很多 (λx.t) 风格的术语,因为这就是 lambda 演算的样子喜欢。

For programmers who haven't encountered functional programming in depth, and don't care to start now, but are mildly curious:

The Y combinator is a formula which lets you implement recursion in a situation where functions can't have names but can be passed around as arguments, used as return values, and defined within other functions.

It works by passing the function to itself as an argument, so it can call itself.

It's part of the lambda calculus, which is really maths but is effectively a programming language, and is pretty fundamental to computer science and especially to functional programming.

The day to day practical value of the Y combinator is limited, since programming languages tend to let you name functions.

In case you need to identify it in a police lineup, it looks like this:

Y = λf.(λx.f (x x)) (λx.f (x x))

You can usually spot it because of the repeated (λx.f (x x)).

The λ symbols are the Greek letter lambda, which gives the lambda calculus its name, and there's a lot of (λx.t) style terms because that's what the lambda calculus looks like.

孤君无依 2024-07-12 02:38:07

JavaScript 中的 y-combinator:

var Y = function(f) {
  return (function(g) {
    return g(g);
  })(function(h) {
    return function() {
      return f(h(h)).apply(null, arguments);
    };
  });
};

var factorial = Y(function(recurse) {
  return function(x) {
    return x == 0 ? 1 : x * recurse(x-1);
  };
});

factorial(5)  // -> 120

编辑
我通过查看代码学到了很多东西,但是如果没有一些背景知识,这一点有点难以接受 - 对此感到抱歉。 有了其他答案提供的一些常识,您就可以开始区分正在发生的事情。

Y 函数是“y 组合器”。 现在看一下使用 Y 的 var Factorial 行。 请注意,您向其传递了一个具有参数的函数(在本示例中为 recurse),该参数稍后也会在内部函数中使用。 参数名称基本上成为内部函数的名称,允许它执行递归调用(因为它在定义中使用了recurse()。)y-combinator 执行了关联匿名内部函数的魔力。函数的参数名称传递给 Y。

有关 Y 如何发挥魔法的完整说明,请查看 链接文章(顺便说一句,不是我写的。)

y-combinator in JavaScript:

var Y = function(f) {
  return (function(g) {
    return g(g);
  })(function(h) {
    return function() {
      return f(h(h)).apply(null, arguments);
    };
  });
};

var factorial = Y(function(recurse) {
  return function(x) {
    return x == 0 ? 1 : x * recurse(x-1);
  };
});

factorial(5)  // -> 120

Edit:
I learn a lot from looking at code, but this one is a bit tough to swallow without some background - sorry about that. With some general knowledge presented by other answers, you can begin to pick apart what is happening.

The Y function is the "y-combinator". Now take a look at the var factorial line where Y is used. Notice you pass a function to it that has a parameter (in this example, recurse) that is also used later on in the inner function. The parameter name basically becomes the name of the inner function allowing it to perform a recursive call (since it uses recurse() in it's definition.) The y-combinator performs the magic of associating the otherwise anonymous inner function with the parameter name of the function passed to Y.

For the full explanation of how Y does the magic, checked out the linked article (not by me btw.)

寻梦旅人 2024-07-12 02:38:07

我想知道尝试从头开始构建这个是否有任何用处。 让我们来看看。 这是一个基本的递归阶乘函数:

function factorial(n) {
    return n == 0 ? 1 : n * factorial(n - 1);
}

让我们重构并创建一个名为 fact 的新函数,它返回一个匿名阶乘计算函数,而不是执行计算本身:

function fact() {
    return function(n) {
        return n == 0 ? 1 : n * fact()(n - 1);
    };
}

var factorial = fact();

这有点奇怪,但没有任何问题。 我们只是在每一步生成一个新的阶乘函数。

这个阶段的递归仍然相当明确。 fact 函数需要知道它自己的名称。 让我们参数化递归调用:

function fact(recurse) {
    return function(n) {
        return n == 0 ? 1 : n * recurse(n - 1);
    };
}

function recurser(x) {
    return fact(recurser)(x);
}

var factorial = fact(recurser);

这很好,但是 recurser 仍然需要知道它自己的名称。 我们也将其参数化:

function recurser(f) {
    return fact(function(x) {
        return f(f)(x);
    });
}

var factorial = recurser(recurser);

现在,我们不直接调用 recurser(recurser),而是创建一个返回其结果的包装函数:

function Y() {
    return (function(f) {
        return f(f);
    })(recurser);
}

var factorial = Y();

我们现在可以去掉 recurser 名称共; 它只是 Y 内部函数的一个参数,可以用函数本身替换:

function Y() {
    return (function(f) {
        return f(f);
    })(function(f) {
        return fact(function(x) {
            return f(f)(x);
        });
    });
}

var factorial = Y();

唯一仍然引用的外部名称是 fact,但现在应该很清楚,它也很容易参数化,创建完整的通用解决方案:

function Y(le) {
    return (function(f) {
        return f(f);
    })(function(f) {
        return le(function(x) {
            return f(f)(x);
        });
    });
}

var factorial = Y(function(recurse) {
    return function(n) {
        return n == 0 ? 1 : n * recurse(n - 1);
    };
});

I wonder if there's any use in attempting to build this from the ground up. Let's see. Here's a basic, recursive factorial function:

function factorial(n) {
    return n == 0 ? 1 : n * factorial(n - 1);
}

Let's refactor and create a new function called fact that returns an anonymous factorial-computing function instead of performing the calculation itself:

function fact() {
    return function(n) {
        return n == 0 ? 1 : n * fact()(n - 1);
    };
}

var factorial = fact();

That's a little weird, but there's nothing wrong with it. We're just generating a new factorial function at each step.

The recursion at this stage is still fairly explicit. The fact function needs to be aware of its own name. Let's parameterize the recursive call:

function fact(recurse) {
    return function(n) {
        return n == 0 ? 1 : n * recurse(n - 1);
    };
}

function recurser(x) {
    return fact(recurser)(x);
}

var factorial = fact(recurser);

That's great, but recurser still needs to know its own name. Let's parameterize that, too:

function recurser(f) {
    return fact(function(x) {
        return f(f)(x);
    });
}

var factorial = recurser(recurser);

Now, instead of calling recurser(recurser) directly, let's create a wrapper function that returns its result:

function Y() {
    return (function(f) {
        return f(f);
    })(recurser);
}

var factorial = Y();

We can now get rid of the recurser name altogether; it's just an argument to Y's inner function, which can be replaced with the function itself:

function Y() {
    return (function(f) {
        return f(f);
    })(function(f) {
        return fact(function(x) {
            return f(f)(x);
        });
    });
}

var factorial = Y();

The only external name still referenced is fact, but it should be clear by now that that's easily parameterized, too, creating the complete, generic, solution:

function Y(le) {
    return (function(f) {
        return f(f);
    })(function(f) {
        return le(function(x) {
            return f(f)(x);
        });
    });
}

var factorial = Y(function(recurse) {
    return function(n) {
        return n == 0 ? 1 : n * recurse(n - 1);
    };
});
┊风居住的梦幻卍 2024-07-12 02:38:07

Y 组合器是一种“函数式”(对其他函数进行操作的函数),当您无法从函数内部引用该函数时,它可以启用递归。 在计算机科学理论中,它概括了递归,抽象了其实现,从而将其与相关函数的实际工作分开。 递归函数不需要编译时名称的好处是一种额外的好处。 =)

这适用于支持lambda 函数的语言。 lambda 基于表达式的性质通常意味着它们不能通过名称引用自身。 通过声明变量、引用它,然后将 lambda 分配给它来完成自引用循环来解决这个问题是很脆弱的。 lambda 变量可以被复制,并重新分配原始变量,这会破坏自引用。

静态类型语言中,Y 组合器的实现和使用都很麻烦( 过程语言通常是这样的),因为通常类型限制需要相关函数的参数数量在编译时就知道。 这意味着必须为需要使用的任何参数计数编写 y 组合器。

下面是一个在 C# 中如何使用和工作 Y-Combinator 的示例。

使用 Y 组合器涉及构建递归函数的“不寻常”方式。 首先,您必须将函数编写为调用预先存在的函数而不是函数本身的一段代码:

// Factorial, if func does the same thing as this bit of code...
x == 0 ? 1: x * func(x - 1);

然后将其转换为一个函数,该函数接受一个要调用的函数,并返回一个执行此操作的函数。 这称为泛函,因为它接受一个函数,并用它执行一项操作,从而产生另一个函数。

// A function that creates a factorial, but only if you pass in
// a function that does what the inner function is doing.
Func<Func<Double, Double>, Func<Double, Double>> fact =
  (recurs) =>
    (x) =>
      x == 0 ? 1 : x * recurs(x - 1);

现在你有一个函数,它接受一个函数,并返回另一个看起来像阶乘的函数,但它不是调用自身,而是调用传递到外部函数的参数。 你如何使它成为阶乘? 将内部函数传递给自身。 Y-Combinator 通过成为一个具有永久名称的函数来实现这一点,它可以引入递归。

// One-argument Y-Combinator.
public static Func<T, TResult> Y<T, TResult>(Func<Func<T, TResult>, Func<T, TResult>> F)
{
  return
    t =>  // A function that...
      F(  // Calls the factorial creator, passing in...
        Y(F)  // The result of this same Y-combinator function call...
              // (Here is where the recursion is introduced.)
        )
      (t); // And passes the argument into the work function.
}

所发生的情况不是阶乘调用本身,而是阶乘调用阶乘生成器(由对 Y-Combinator 的递归调用返回)。 根据 t 的当前值,从生成器返回的函数将使用 t - 1 再次调用生成器,或者仅返回 1,终止递归。

它复杂而神秘,但在运行时一切都会发生变化,其工作的关键是“延迟执行”,以及分解递归以跨越两个函数。 内部 F 作为参数传递仅在必要时在下一次迭代中调用。

A Y-combinator is a "functional" (a function that operates on other functions) that enables recursion, when you can't refer to the function from within itself. In computer-science theory, it generalizes recursion, abstracting its implementation, and thereby separating it from the actual work of the function in question. The benefit of not needing a compile-time name for the recursive function is sort of a bonus. =)

This is applicable in languages that support lambda functions. The expression-based nature of lambdas usually means that they cannot refer to themselves by name. And working around this by way of declaring the variable, refering to it, then assigning the lambda to it, to complete the self-reference loop, is brittle. The lambda variable can be copied, and the original variable re-assigned, which breaks the self-reference.

Y-combinators are cumbersome to implement, and often to use, in static-typed languages (which procedural languages often are), because usually typing restrictions require the number of arguments for the function in question to be known at compile time. This means that a y-combinator must be written for any argument count that one needs to use.

Below is an example of how the usage and working of a Y-Combinator, in C#.

Using a Y-combinator involves an "unusual" way of constructing a recursive function. First you must write your function as a piece of code that calls a pre-existing function, rather than itself:

// Factorial, if func does the same thing as this bit of code...
x == 0 ? 1: x * func(x - 1);

Then you turn that into a function that takes a function to call, and returns a function that does so. This is called a functional, because it takes one function, and performs an operation with it that results in another function.

// A function that creates a factorial, but only if you pass in
// a function that does what the inner function is doing.
Func<Func<Double, Double>, Func<Double, Double>> fact =
  (recurs) =>
    (x) =>
      x == 0 ? 1 : x * recurs(x - 1);

Now you have a function that takes a function, and returns another function that sort of looks like a factorial, but instead of calling itself, it calls the argument passed into the outer function. How do you make this the factorial? Pass the inner function to itself. The Y-Combinator does that, by being a function with a permanent name, which can introduce the recursion.

// One-argument Y-Combinator.
public static Func<T, TResult> Y<T, TResult>(Func<Func<T, TResult>, Func<T, TResult>> F)
{
  return
    t =>  // A function that...
      F(  // Calls the factorial creator, passing in...
        Y(F)  // The result of this same Y-combinator function call...
              // (Here is where the recursion is introduced.)
        )
      (t); // And passes the argument into the work function.
}

Rather than the factorial calling itself, what happens is that the factorial calls the factorial generator (returned by the recursive call to Y-Combinator). And depending on the current value of t the function returned from the generator will either call the generator again, with t - 1, or just return 1, terminating the recursion.

It's complicated and cryptic, but it all shakes out at run-time, and the key to its working is "deferred execution", and the breaking up of the recursion to span two functions. The inner F is passed as an argument, to be called in the next iteration, only if necessary.

网白 2024-07-12 02:38:07

If you're ready for a long read, Mike Vanier has a great explanation. Long story short, it allows you to implement recursion in a language that doesn't necessarily support it natively.

黯然 2024-07-12 02:38:07

我已从 http://www.mail-archive.com/ 中提取了此内容[email protected]/msg02716.html 这是我几年前写的解释。

我将在本例中使用 JavaScript,但许多其他语言也可以使用。

我们的目标是能够编写 1 的递归函数
变量仅使用 1 个变量的函数,并且不使用
作业、按名称定义事物等(为什么这是我们的
目标是另一个问题,我们就以此为目标
我们面临的挑战。)似乎不可能,是吧? 作为
举个例子,让我们实现阶乘。

第一步是说我们可以很容易地做到这一点,如果我们
欺骗了一点。 使用 2 个变量的函数和
我们至少可以避免使用
设置递归的赋值。

// Here's the function that we want to recurse.
X = function (recurse, n) {
  if (0 == n)
    return 1;
  else
    return n * recurse(recurse, n - 1);
};

// This will get X to recurse.
Y = function (builder, n) {
  return builder(builder, n);
};

// Here it is in action.
Y(
  X,
  5
);

现在让我们看看我们是否可以少作弊。 首先我们使用的是
分配,但我们不需要。 我们可以只写 X 和
Y 内联。

// No assignment this time.
function (builder, n) {
  return builder(builder, n);
}(
  function (recurse, n) {
    if (0 == n)
      return 1;
    else
      return n * recurse(recurse, n - 1);
  },
  5
);

但我们使用 2 个变量的函数来得到 1 个变量的函数
多变的。 我们能解决这个问题吗? 嗯,一个聪明的人,名叫
Haskell Curry 有一个巧妙的技巧,如果你有良好的高阶
那么你只需要 1 个变量的函数。 这
证明是你可以从 2(或更多的函数)中得到
一般情况)变量到 1 个变量,纯
像这样的机械文本转换:

// Original
F = function (i, j) {
  ...
};
F(i,j);

// Transformed
F = function (i) { return function (j) {
  ...
}};
F(i)(j);

其中...保持完全相同。 (这个技巧叫做
以它的发明者的名字“柯里化”。 Haskell 语言也是
以哈斯克尔咖喱命名。 将其归档在无用的琐事下。)
现在只要在任何地方应用这个转换,我们就可以得到
我们的最终版本。

// The dreaded Y-combinator in action!
function (builder) { return function (n) {
  return builder(builder)(n);
}}(
  function (recurse) { return function (n) {
    if (0 == n)
      return 1;
    else
      return n * recurse(recurse)(n - 1);
  }})(
  5
);

请随意尝试一下。 返回的alert(),将其绑定到一个按钮,等等。
该代码递归地计算阶乘,而不使用
2 个变量的赋值、声明或函数。 (但
试图追踪它是如何工作的可能会让你头晕。
并传递它,没有推导,只是稍微重新格式化
将导致代码肯定会令人困惑和困惑。)

您可以将递归定义阶乘的 4 行替换为
您想要的任何其他递归函数。

I've lifted this from http://www.mail-archive.com/[email protected]/msg02716.html which is an explanation I wrote several years ago.

I'll use JavaScript in this example, but many other languages will work as well.

Our goal is to be able to write a recursive function of 1
variable using only functions of 1 variables and no
assignments, defining things by name, etc. (Why this is our
goal is another question, let's just take this as the
challenge that we're given.) Seems impossible, huh? As
an example, let's implement factorial.

Well step 1 is to say that we could do this easily if we
cheated a little. Using functions of 2 variables and
assignment we can at least avoid having to use
assignment to set up the recursion.

// Here's the function that we want to recurse.
X = function (recurse, n) {
  if (0 == n)
    return 1;
  else
    return n * recurse(recurse, n - 1);
};

// This will get X to recurse.
Y = function (builder, n) {
  return builder(builder, n);
};

// Here it is in action.
Y(
  X,
  5
);

Now let's see if we can cheat less. Well firstly we're using
assignment, but we don't need to. We can just write X and
Y inline.

// No assignment this time.
function (builder, n) {
  return builder(builder, n);
}(
  function (recurse, n) {
    if (0 == n)
      return 1;
    else
      return n * recurse(recurse, n - 1);
  },
  5
);

But we're using functions of 2 variables to get a function of 1
variable. Can we fix that? Well a smart guy by the name of
Haskell Curry has a neat trick, if you have good higher order
functions then you only need functions of 1 variable. The
proof is that you can get from functions of 2 (or more in the
general case) variables to 1 variable with a purely
mechanical text transformation like this:

// Original
F = function (i, j) {
  ...
};
F(i,j);

// Transformed
F = function (i) { return function (j) {
  ...
}};
F(i)(j);

where ... remains exactly the same. (This trick is called
"currying" after its inventor. The language Haskell is also
named for Haskell Curry. File that under useless trivia.)
Now just apply this transformation everywhere and we get
our final version.

// The dreaded Y-combinator in action!
function (builder) { return function (n) {
  return builder(builder)(n);
}}(
  function (recurse) { return function (n) {
    if (0 == n)
      return 1;
    else
      return n * recurse(recurse)(n - 1);
  }})(
  5
);

Feel free to try it. alert() that return, tie it to a button, whatever.
That code calculates factorials, recursively, without using
assignment, declarations, or functions of 2 variables. (But
trying to trace how it works is likely to make your head spin.
And handing it, without the derivation, just slightly reformatted
will result in code that is sure to baffle and confuse.)

You can replace the 4 lines that recursively define factorial with
any other recursive function that you want.

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文