U 组合子
通过将函数作为参数传递给自身,函数可以使用其参数而不是其名称来递归!所以给U的函数应该至少有一个参数可以绑定到函数(本身)。
在下面的例子中,我们没有退出条件,所以我们将无限循环直到堆栈溢出发生
const U = f => f (f) // call function f with itself as an argument
U (f => (console.log ('stack overflow imminent!'), U (f)))
我们可以使用多种技术停止无限递归。在这里,我将编写我们的匿名函数来返回等待输入的另一个匿名函数;在这种情况下,一些数字。当提供一个数字时,如果大于0,我们会继续循环,否则返回0。
const log = x => (console.log (x), x)
const U = f => f (f)
// when our function is applied to itself, we get the inner function back
U (f => x => x > 0 ? U (f) (log (x - 1)) : 0)
// returns: (x => x > 0 ? U (f) (log (x - 1)) : 0)
// where f is a reference to our outer function
// watch when we apply an argument to this function, eg 5
U (f => x => x > 0 ? U (f) (log (x - 1)) : 0) (5)
// 4 3 2 1 0
这里没有立即明显的是,当我们的函数第一次使用U 组合器应用于自身时,它返回一个等待第一个输入的函数。如果我们给它起个名字,可以有效地使用 lambdas(匿名函数)构造递归函数
const log = x => (console.log (x), x)
const U = f => f (f)
const countDown = U (f => x => x > 0 ? U (f) (log (x - 1)) : 0)
countDown (5)
// 4 3 2 1 0
countDown (3)
// 2 1 0
只有这不是直接递归——一个使用自己的名字调用自己的函数。我们对countDown 的定义并没有在其主体内部引用自身,并且仍然可以进行递归
// direct recursion references itself by name
const loop = (params) => {
if (condition)
return someValue
else
// loop references itself to recur...
return loop (adjustedParams)
}
// U combinator does not need a named reference
// no reference to `countDown` inside countDown's definition
const countDown = U (f => x => x > 0 ? U (f) (log (x - 1)) : 0)
如何使用 U 组合器从现有函数中删除自引用
在这里,我将向您展示如何获取一个使用对自身的引用的递归函数,并将其更改为使用 U 组合符来代替自引用的函数
const factorial = x =>
x === 0 ? 1 : x * factorial (x - 1)
console.log (factorial (5)) // 120
现在使用 U 组合器替换对factorial 的内部引用
const U = f => f (f)
const factorial = U (f => x =>
x === 0 ? 1 : x * U (f) (x - 1))
console.log (factorial (5)) // 120
基本的替换模式是这样的。记下,我们将在下一节中使用类似的策略
// self reference recursion
const foo = x => ... foo (nextX) ...
// remove self reference with U combinator
const foo = U (f => x => ... U (f) (nextX) ...)
Y 组合子
相关:the U and Y combinators explained using a mirror analogy
在上一节中,我们看到了如何使用 U 组合子将自引用递归转换为不依赖于命名函数的递归函数。必须记住始终将函数作为第一个参数传递给自身,这有点令人烦恼。嗯,Y-combinator 建立在 U-combinator 的基础上,去掉了那个乏味的部分。这是一件好事,因为消除/减少复杂性是我们制作函数的主要原因
首先,让我们推导出我们自己的 Y 组合器
// standard definition
const Y = f => f (Y (f))
// prevent immediate infinite recursion in applicative order language (JS)
const Y = f => f (x => Y (f) (x))
// remove reference to self using U combinator
const Y = U (h => f => f (x => U (h) (f) (x)))
现在我们将看看它的用法与 U-combinator 的比较。注意,为了重复,我们可以简单地调用f ()而不是U (f)
const U = f => f (f)
const Y = U (h => f => f (x => U (h) (f) (x)))
Y (f => (console.log ('stack overflow imminent!'), f ()))
现在我将使用 Y 演示 countDown 程序 - 你会看到程序几乎相同,但 Y 组合器让事情变得更简洁
const log = x => (console.log (x), x)
const U = f => f (f)
const Y = U (h => f => f (x => U (h) (f) (x)))
const countDown = Y (f => x => x > 0 ? f (log (x - 1)) : 0)
countDown (5)
// 4 3 2 1 0
countDown (3)
// 2 1 0
现在我们也会看到factorial
const U = f => f (f)
const Y = U (h => f => f (x => U (h) (f) (x)))
const factorial = Y (f => x =>
x === 0 ? 1 : x * f (x - 1))
console.log (factorial (5)) // 120
如您所见,f 成为递归本身的机制。为了递归,我们像普通函数一样调用它。我们可以用不同的参数多次调用它,结果仍然是正确的。而且由于是普通的函数参数,我们可以随意命名,比如下面的recur-
const U = f => f (f)
const Y = U (h => f => f (x => U (h) (f) (x)))
const fibonacci = Y (recur => n =>
n < 2 ? n : recur (n - 1) + (n - 2))
console.log (fibonacci (10)) // 55
多于 1 个参数的 U 和 Y 组合器
在上面的示例中,我们看到了如何循环并传递参数来跟踪计算的“状态”。但是如果我们需要跟踪其他状态怎么办?
我们可以使用数组之类的复合数据...
const U = f => f (f)
const Y = U (h => f => f (x => U (h) (f) (x)))
const fibonacci = Y (f => ([a, b, x]) =>
x === 0 ? a : f ([b, a + b, x - 1]))
// starting with 0 and 1, generate the 7th number in the sequence
console.log (fibonacci ([0, 1, 7]))
// 0 1 1 2 3 5 8 13
但这很糟糕,因为它暴露了内部状态(计数器a 和b)。如果我们可以拨打fibonacci (7) 来获得我们想要的答案,那就太好了。
使用我们对柯里化函数(一元(1 参数)函数的序列)的了解,我们可以轻松实现我们的目标,而无需修改 Y 的定义或依赖复合数据或高级语言功能。
请看下面fibonacci 的定义。我们立即应用0 和1,它们分别绑定到a 和b。现在斐波那契只是等待提供最后一个参数,该参数将绑定到x。当我们递归时,我们必须调用f (a) (b) (x)(而不是f (a,b,x)),因为我们的函数是柯里化的形式。
const U = f => f (f)
const Y = U (h => f => f (x => U (h) (f) (x)))
const fibonacci = Y (f => a => b => x =>
x === 0 ? a : f (b) (a + b) (x - 1)) (0) (1)
console.log (fibonacci (7))
// 0 1 1 2 3 5 8 13
这种模式对于定义各种函数很有用。下面我们将看到使用Y 组合子(range 和reduce)以及reduce、map 的派生函数定义的另外两个函数。
const U = f => f (f)
const Y = U (h => f => f (x => U (h) (f) (x)))
const range = Y (f => acc => min => max =>
min > max ? acc : f ([...acc, min]) (min + 1) (max)) ([])
const reduce = Y (f => g => y => ([x,...xs]) =>
x === undefined ? y : f (g) (g (y) (x)) (xs))
const map = f =>
reduce (ys => x => [...ys, f (x)]) ([])
const add = x => y => x + y
const sq = x => x * x
console.log (range (-2) (2))
// [ -2, -1, 0, 1, 2 ]
console.log (reduce (add) (0) ([1,2,3,4]))
// 10
console.log (map (sq) ([1,2,3,4]))
// [ 1, 4, 9, 16 ]
都是匿名的天啊
因为我们在这里使用的是纯函数,所以我们可以用任何命名函数来代替它的定义。观察当我们采用斐波那契并用它们的表达式替换命名函数时会发生什么
/* const U = f => f (f)
*
* const Y = U (h => f => f (x => U (h) (f) (x)))
*
* const fibonacci = Y (f => a => b => x => x === 0 ? a : f (b) (a + b) (x - 1)) (0) (1)
*
*/
/*
* given fibonacci (7)
*
* replace fibonacci with its definition
* Y (f => a => b => x => x === 0 ? a : f (b) (a + b) (x - 1)) (0) (1) (7)
*
* replace Y with its definition
* U (h => f => f (x => U (h) (f) (x))) (f => a => b => x => x === 0 ? a : f (b) (a + b) (x - 1)) (0) (1) (7)
//
* replace U with its definition
* (f => f (f)) U (h => f => f (x => U (h) (f) (x))) (f => a => b => x => x === 0 ? a : f (b) (a + b) (x - 1)) (0) (1) (7)
*/
let result =
(f => f (f)) (h => f => f (x => h (h) (f) (x))) (f => a => b => x => x === 0 ? a : f (b) (a + b) (x - 1)) (0) (1) (7)
console.log (result) // 13
你有它——fibonacci (7)只使用匿名函数递归计算