与其对 Crockford 的 Y 进行逆向工程,不如先从其目的开始理解 Y。当我们从一个意图开始并按照我们的方式向前 找到解决方案时,我们就可以开始了解Y 的工作原理。
“我想写一个递归公式……不使用递归”
早在人们广泛使用计算机之前,数学家和逻辑学家就面临着这个问题。这就是Y 组合器解决的问题,但它是如何做到的呢?让我们先从我们要写的递归“公式”开始——
const fact = n =>
n <= 2
? n
: n * fact (n - 1) // <-- recursive!
fact 以上是根据自身定义的。我们可以通过使用一个额外的参数来控制递归来删除这个自引用,recur -
const fact = n =>
const fact = recur => n =>
n <= 2
? n
: n * fact (n - 1)
: n * ... (n - 1)
但是recur 应该是什么?以及我们如何填写...?认识U。 U 只是将一个函数应用于自身 -
const U = f =>
f (f) // <-- apply f to itself
const fact = recur => n =>
n <= 2
? n
: n * U (recur) (n - 1) // <-- apply recur to itself
console.log (U (fact) (5)) // <-- apply fact to itself
// 120
上面我们调用U (fact) (5) 来计算结果。重要的是要注意,当我们定义 fact = ... 时,我们可以应用 U -
const U = f =>
f (f)
const fact = U (recur => n => // <-- apply U directly on lambda
n <= 2
? n
: n * U (recur) (n - 1))
console.log (fact (5)) // <-- call fact normally
// 120
使用U,我们现在有了一种编写递归表达式的通用方法-
U (recur => ... U (recur) ...)
“我不喜欢这样,我必须记住使用U (recur) (...)。我可以直接打电话给recur (...)吗?”
U 是一个非常简单的机制,但确实有点麻烦,因为我们必须记住每次重复时都将recur“反射”回自身。这就是Y 组合子与U 的不同之处。 Y 说,
- 给我函数
f,它要求递归机制
- 我会给你一个递归机制,
x => ...,当你调用它时,我会处理将x传递给一些U (recur) (...)
const Y = f =>
f (x => ...) // <-- instead of f (f)...
就像我们在上面看到的 fact 一样,我们可以使用 U 编写递归表达式 -
U (recur => ... U (recur) ...)
因此,某个函数f 的Y 将创建一个递归机制U (recur => ...),它会使用函数f 调用x => ...,当应用该函数时,将使用@ 与x 一起递归987654363@-
const Y = f =>
// f (x => ... (x))
// ======= ====
// | |
// v v
// U (recur => ... U (recur) ...)
U (recur => f (x => U (recur) (x)))
让我们看看下面的Y -
const U = f =>
f (f)
const Y = f =>
U (recur => f (x => U (recur) (x)))
const fact = Y (recur => n =>
n <= 2
? n
: n * recur (n - 1)) // <-- call "recur" normally
console.log (fact (5)) // <-- call "fact" normally
// 120
番茄,番茄
Crockford 将 Y-combinator 称为 “......计算机科学中最奇怪和最奇妙的神器之一”,但我认为那是因为他不理解它并继续复制/粘贴您在互联网上看到的相同Y 示例。没有什么奇怪,除非你以一种混淆所有含义的方式编写它 -
// replace IIFE with U
function Y(le) {
return (
function (f) { return f(f); }
U
(function (f) {
return le(function (x) {
return f(f)(x);
return U(f)(x)
});
})
)
}
// remove unnecessary outer (...)
function Y(le) {
return U(function (f) {
return le(function (x) {
return U(f)(x)
})
})
}
// simplified arrow expression
const Y = le =>
U (f => le (x => U (f) (x))
// alpha rename "f" to "recur"
const Y = le =>
U (recur => le (x => U (recur) (x))
// alpha rename "le" to "f"
const Y = f =>
U (recur => f (x => U (recur) (x))
与我们上面的Y 版本相匹配 -
const Y = f =>
U (recur => f (x => U (recur) (x)))
没有你
我们可以扩展U 使得Y 有一个独立的定义-
// starting with
const Y = f =>
U (recur => f (x => U (recur) (x)))
// expand inner U
const Y = f =>
U (recur => f (x => recur (recur) (x)))
// expand outer U
const Y = f =>
(recur => f (x => recur (recur) (x)))
(recur => f (x => recur (recur) (x)))
我们又回到了 IIFE,但这次我们的定义更接近 Haskell Curry 的 original definition -
const Y = f =>
(recur => f (x => recur (recur) (x)))
(recur => f (x => recur (recur) (x)))
const fact = Y (recur => n =>
n <= 2
? n
: n * recur (n - 1))
console.log (fact (5))
// 120
扩展我们的 Y 直觉
与几乎所有其他 Y 教程一样,Crockford 演示了在具有单个参数的函数上使用 Y。除了对Y 的初步理解之外,还有很多潜在的潜伏。我将向您展示带有 2 个甚至 3 个参数的简单函数 -
const Y = f =>
(recur => f (x => recur (recur) (x)))
(recur => f (x => recur (recur) (x)))
const range1 = Y (recur => m => n =>
m > n
? []
: [ m, ...recur (m + 1) (n) ])
const range2 = Y (recur => r => m => n =>
m > n
? r
: recur ([ ...r, m ]) (m + 1) (n)) ([])
const fib = Y (recur => a => b => n =>
n === 0
? a
: recur (b) (a + b) (n - 1)) (0) (1)
console.log (range1 (5) (10))
// [ 5, 6, 7, 8, 9, 10 ]
console.log (range2 (5) (10))
// [ 5, 6, 7, 8, 9, 10 ]
console.log (fib (10))
// 55
Y可以表达的程序复杂度真的没有限制-
const Y = f =>
(recur => f (x => recur (recur) (x)))
(recur => f (x => recur (recur) (x)))
const reduce = Y (recur => i => f => state => a =>
i >= a.length
? state
: recur (i + 1) (f) (f (state, a[i])) (a)) (0)
const filter = f =>
reduce ((r, x) => f (x) ? [ ...r, x ] : r) ([])
const odds =
filter (n => n & 1)
console.log (odds ([ 0, 1, 2, 3, 4, 5, 6 ]))
// [ 1, 3, 5 ]