Y 组合器是从事物的“功能”方面来看的计算机科学概念。大多数程序员对组合子一无所知,即使他们听说过它们。
- 什么是 Y 组合器?
- 组合器如何工作?
- 它们有什么用?
- 它们在程序语言中有用吗?
Y 组合器是从事物的“功能”方面来看的计算机科学概念。大多数程序员对组合子一无所知,即使他们听说过它们。
Y-combinator 是一种“函数式”(一种对其他函数进行操作的函数),当您无法从其内部引用该函数时,它可以实现递归。在计算机科学理论中,它概括了递归,抽象了它的实现,从而将它与相关函数的实际工作分开。递归函数不需要编译时名称的好处是一种奖励。=)
这适用于支持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作为参数传递,仅在必要时在下一次迭代中调用。
如果您准备好阅读全文,Mike Vanier 有一个很好的解释。长话短说,它允许您用一种不一定支持本机的语言来实现递归。
我从http://www.mail-archive.com/boston-pm@mail.pm.org/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);
其中...保持完全相同。(这个技巧在它的发明者之后被称为“currying”。Haskell 语言也以 Haskell Curry 命名。在无用的琐事下归档。)现在只要在任何地方应用这个转换,我们就得到了我们的最终版本。
// 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 行替换为您想要的任何其他递归函数。
我想知道尝试从头开始构建它是否有任何用处。让我们来看看。这是一个基本的递归阶乘函数:
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);
};
});
对于没有深入接触过函数式编程的程序员,也不想从现在开始,但有点好奇:
Y 组合子是一个公式,它允许您在函数不能有名称但可以作为参数传递、用作返回值以及在其他函数中定义的情况下实现递归。
它通过将函数作为参数传递给自身来工作,因此它可以调用自身。
它是 lambda 演算的一部分,它实际上是数学,但实际上是一种编程语言,对于计算机科学尤其是函数式编程非常基础。
Y 组合器的日常实用价值是有限的,因为编程语言倾向于让您命名函数。
如果您需要在警察阵容中识别它,它看起来像这样:
Y = λf.(λx.f (xx)) (λx.f (xx))
由于重复,您通常可以发现它(λx.f (x x))
。
这些λ
符号是希腊字母 lambda,它给了 lambda 演算它的名字,并且有很多(λx.t)
风格术语,因为这就是 lambda 演算的样子。
JavaScript中的 y 组合器:
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 组合器”。现在看一下var factorial
使用 Y 的行。请注意,您将一个函数传递给它,该函数具有一个参数(在本例中recurse
为 ),该参数稍后也会在内部函数中使用。参数名称基本上成为内部函数的名称,允许它执行递归调用(因为它recurse()
在它的定义中使用。)y 组合器执行将其他匿名内部函数与传递给的函数的参数名称相关联的魔力Y。
有关 Y 如何发挥魔力的完整说明,请查看链接文章(顺便说一句,不是我写的。)
定点组合器是一个高阶函数fix
,根据定义满足等价
forall f. fix f = f (fix f)
fix f
表示x
定点方程的解
x = f x
自然数的阶乘可以证明为
fact 0 = 1
fact n = n * fact (n - 1)
使用fix
,可以在没有匿名自引用的情况下推导出对一般/μ-递归函数的任意建设性证明。
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 演算形式主义包括上下文无关文法
E ::= v Variable
| λ v. E Abstraction
| E E Application
其中v
变量的范围,以及beta和eta 减少规则
(λ x. B) E -> B[x := E] Beta
λ x. E x -> E if x doesn’t occur free in E Eta
Beta减少用表达式(“argument”)替换x
抽象(“函数”)主体中变量的所有自由出现。Eta 减少消除了冗余抽象。它有时会从形式主义中省略。一个不适用任何归约规则的不可归约表达式是正常形式或规范形式。B
E
λ x y. E
是简写
λ x. λ y. E
(抽象多元性),
E F G
是简写
(E F) G
(应用程序左关联性),
λ x. x
和
λ y. y
是alpha 等效的。
抽象和应用是 lambda 演算中仅有的两个“语言原语”,但它们允许对任意复杂的数据和操作进行编码。
Church 数字是自然数的编码,类似于 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
. . .
正式证明
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 演算中,组合子是不包含自由变量的抽象。最简单的:I
,身份组合器
λ x. x
与恒等函数同构
id x = x
这样的组合子是组合子演算的原始算子,如 SKI 系统。
S = λ x y z. x z (y z)
K = λ x y. x
I = λ x. x
Beta 减少不是强标准化;并非所有可约表达式,“redexes”,在 beta 约简下都收敛到范式。一个简单的例子是欧米茄ω
组合器的不同应用
λ 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))
在急切的应用阶贝塔减少下发散
= (λ 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 _|_ = _|_
但在惰性正态贝塔归约下收敛
= (λ l. ((λ i. i) a)) ((λ x. x x) (λ y. y y))
= (λ l. a) ((λ x. x x) (λ y. y y))
= a
如果表达式具有范式,则正常阶的 beta 归约会找到它。
定点组合器的基本性质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
(乘法延迟,汇合)
对于 Churchian 无类型 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])
其他答案对此提供了非常简洁的答案,但没有一个重要的事实:您不需要以这种复杂的方式以任何实用语言实现定点组合器,这样做没有任何实际目的(除了“看,我知道 Y 组合器是”)。这是一个重要的理论概念,但没有什么实用价值。
这是 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);
Y-Combinator 是磁通电容器的另一个名称。
我已经在 Clojure 和 Scheme 中为 Y-Combinator 编写了一种“白痴指南”,以帮助自己掌握它。他们受到“小阴谋家”中材料的影响
在方案中: https ://gist.github.com/z5h/238891
或 Clojure: https ://gist.github.com/z5h/5102747
这两篇教程都是代码中散布着注释,应该剪切和粘贴到你最喜欢的编辑器中。
作为组合器的新手,我发现Mike Vanier 的文章(感谢 Nicholas Mancuso)非常有帮助。我想写一个总结,除了记录我的理解之外,如果它可以帮助其他人,我会很高兴。
以阶乘为例,我们使用以下almost-factorial
函数来计算 number 的阶乘x
:
def almost-factorial f x = if iszero x
then 1
else * x (f (- x 1))
在上面的伪代码almost-factorial
中,传入函数f
和数字x
(almost-factorial
被柯里化了,因此可以看作是传入函数f
并返回一个 1-arity 函数)。
当almost-factorial
计算x
的阶乘时,它将阶乘的计算委托x - 1
给函数f
并将结果与x
(在这种情况下,它将 (x - 1) 的结果与 x 相乘)累加。
它可以看作是almost-factorial
接受了一个糟糕版本的阶乘函数(它只能计算直到 number x - 1
)并返回一个不太糟糕的阶乘版本(它计算直到 number x
)。如这种形式:
almost-factorial crappy-f = less-crappy-f
如果我们反复将不那么糟糕的阶乘传递给almost-factorial
,我们最终会得到我们想要的阶乘函数f
。它可以被认为是:
almost-factorial f = f
almost-factorial f = f
意思是函数f
的固定点almost-factorial
。
这是查看上述函数关系的一种非常有趣的方式,这对我来说是一个惊喜时刻。(如果你还没有,请阅读 Mike 关于定点的帖子)
概括地说,我们有一个非递归函数fn
(比如我们的几乎阶乘),我们有它的定点函数fr
(比如我们的 f),那么Y
当你给出 时Y
fn
,会Y
返回 的定点函数fn
。
所以总而言之(通过假设fr
只接受一个参数来简化;在递归中x
退化为x - 1
, ...):x - 2
fn
: def fn fr x = ...accumulate x with result from (fr (- x 1))
,这是几乎有用的函数 - 虽然我们不能fn
直接在 上使用x
,但它很快就会有用。这个非递归fn
使用一个函数fr
来计算它的结果fn fr = fr
,fr
是 的不动点fn
,fr
是有用的函数, 我们可以使用fr
onx
来得到我们的结果Y fn = fr
,Y
返回一个函数的固定点,Y
把我们几乎有用的函数fn
变成有用的 fr
Y
(不包括在内)我将跳过推导Y
并去理解Y
。Mike Vainer 的帖子有很多细节。
Y
Y
定义为(以lambda 演算格式):
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
对我来说,这个设置的神奇部分是:
fn
并且fr
相互依赖:fr
“包裹”fn
在里面,每次fr
都用于计算x
,它“产生”(“提升”?)fn
并将计算委托给那个fn
(传递自身fr
和x
);另一方面,fn
取决于fr
和用于fr
计算较小问题的结果x-1
。fr
用于定义fn
时(在其操作中fn
使用时),实际尚未定义。fr
fr
fn
定义了真正的业务逻辑。基于fn
,Y
创建fr
- 特定形式的辅助函数 - 以促进fn
递归方式的计算。它帮助我目前理解Y
这种方式,希望它有所帮助。
顺便说一句,我还发现An Introduction to Functional Programming Through Lambda Calculus这本书非常好,我只是其中的一部分,而且我无法Y
在书中理解这一事实导致我写了这篇文章。
以下是原始问题的答案,由 Nicholas Mancuso的答案中提到的文章(完全值得一读)以及其他答案汇编而成 :
什么是 Y 组合器?
Y-combinator 是一个“函数式”(或高阶函数——一个对其他函数进行操作的函数),它接受一个参数,这是一个非递归函数,并返回该函数的一个版本递归的。
有点递归=),但更深入的定义:
组合器——只是一个没有自由变量的 lambda 表达式。
自由变量——是一个不是绑定变量的变量。
绑定变量 — 包含在 lambda 表达式主体内的变量,该变量名称作为其参数之一。
另一种思考方式是,combinator 就是这样一个 lambda 表达式,在其中你可以在任何地方用它的定义替换组合器的名称,并且一切仍然有效(如果组合器会包含对自身的引用,在 lambda 主体内)。
Y-combinator 是一个定点组合器。
函数的不动点是函数域中由函数映射到自身的元素。
也就是说,c
是函数的一个不动点f(x)
iff(c) = c
这意味着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(x x)
,然后将这个 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 组合器本身没有' t 有一个简单的静态类型。这超出了本文的范围,所以我不再赘述
正如Chris Ammerman 所提到的:大多数过程语言都有静态类型。
所以回答这个问题 - 不是真的。
定点组合器(或定点运算符)是计算其他函数的不动点的高阶函数。此操作与编程语言理论相关,因为它允许以重写规则的形式实现递归,而无需语言运行时引擎的明确支持。(来源维基百科)
y-combinator 实现匿名递归。所以而不是
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-combinator 仅适用于按名称调用的语言。如果您想在任何普通的按值调用语言中使用它,那么您将需要相关的 z-combinator(y-combinator 将发散/无限循环)。
this-operator 可以简化你的生活:
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)
.
我认为回答这个问题的最好方法是选择一种语言,比如 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 解决了什么问题。