肉丁土豆园地

静谧编程花园 - The Secret Garden
Y 组合子是怎么推导出来的

如果你学过一些函数式,或者了解过一点 lambda 演算(没了解过也可以继续往下读),你应该知道 for while 之类的循环总是以递归的形式出现,而它的诀窍在于一个叫 \(Y\) 组合子的东西。

但是,每当看到 \(Y\) 组合子那复杂的形式,你就算能理解怎么用它,估计也很难想象出它是怎么被推导出来的。

这篇文章就提供了一个更加具象的推导的思路。

推导

总之让我们先别去思考什么演算什么子了,来考虑一下现实的东西。

斐波那契数列是递归,用普通编程语言你一般可以写成

card(dad_yc)

card(JS+λ)

const feb = n =>
    n <= 2
        ? 1
        : feb(n - 1) + feb(n - 2);

card(JS)

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

card(Python+λ)

feb = lambda n: (
    1 if n <= 2
    else feb(n - 1) + feb(n - 2)
)

card(Python)

def feb(n):
    if n <= 2:
        return 1
    else:
        return feb(n - 1) + feb(n - 2)

可以发现,我们通过 feb 这个名字,在函数内部拿到了函数自己。
现在给你一个挑战:如果 feb 不知道自己叫 feb ,怎么样才能在它的内部拿到自己并调用?

一个很好的思路是在调用自己的时候把自己也作为一个参数传进去,这样下一次的自己就可以调用自己了。
至于第一次调用自己的时候怎么办?再套个函数包装一下就行。

card(dad_yc)

card(JS+λ)

const febWithSelf = (self, n) =>
    n <= 2
        ? 1
        : self(self, n - 1) + self(self, n - 2);

const feb = n => febWithSelf(febWithSelf, n);

card(JS)

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

function feb(n) {
    return febWithSelf(febWithSelf, n);
}

card(Python+λ)

feb_with_self = lambda self, n: (
    1 if n <= 2
    else self(self, n - 1) + self(self, n - 2)
)

feb = lambda n: feb_with_self(feb_with_self, n)

card(Python)

def feb_with_self(self, n):
    if n <= 2:
        return 1
    else:
        return self(self, n - 1) + self(self, n - 2)

def feb(n):
    return feb_with_self(feb_with_self, n)

如果你学过什么叫做柯里化,那你应该知道 f(a, b)f(a)(b) 是一个意思——如果你没学过,那现在我告诉你了。所以我们可以改写程序为

card(dad_yc)

card(JS+λ)

const febWithSelf = self => n =>
    n <= 2
        ? 1
        : self(self)(n - 1) + self(self)(n - 2);

const feb = febWithSelf(febWithSelf);

card(JS)

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

const feb = febWithSelf(febWithSelf);

card(Python+λ)

feb_with_self = lambda self: lambda n: (
    1 if n <= 2
    else self(self)(n - 1) + self(self)(n - 2)
)

feb = feb_with_self(feb_with_self)

card(Python)

def feb_with_self(self):
    def do_feb(n):
        if n <= 2:
            return 1
        else:
            return self(self)(n - 1) + self(self)(n - 2)
    return do_feb

feb = feb_with_self(feb_with_self)

发现一堆 self(self) 太冗长了有没有,柯里化之后我们就可以把 self(self) 变成 f

card(dad_yc)

card(JS+λ)

const febOnly = f => n =>
    n <= 2
        ? 1
        : f(n - 1) + f(n - 2);

const febWithSelf = self => n =>
    febOnly(self(self))(n);

const feb = febWithSelf(febWithSelf);

card(JS)

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

function febWithSelf(self) {
    return function (n) {
        return febOnly(self(self))(n);
    }
}

const feb = febWithSelf(febWithSelf);

card(Python+λ)

feb_only = lambda f: lambda n: (
    1 if n <= 2
    else f(n - 1) + f(n - 2)
)

feb_with_self = lambda self: lambda n: (
    feb_only(self(self))(n)
)

feb = feb_with_self(feb_with_self)

card(Python)

def feb_only(f):
    def calc(n):
        if n <= 2:
            return 1
        else:
            return f(n - 1) + f(n - 2)
    return calc

def feb_with_self(self):
    def do_feb(n):
        return feb_only(self(self))(n)
    return do_feb

feb = feb_with_self(feb_with_self)

不错,你会发现 febOnly 被提取出来了,那也就是说诸如 factorialOnly hanoiOnly 之类各种递归函数也都可以用这种方法?我们下一遍可以来把 febOnly 抽成参数,先来在 feb 里展开 febWithSelf 的定义,

card(dad_yc)

card(JS+λ)

const febOnly = f => n =>
    n <= 2
        ? 1
        : f(n - 1) + f(n - 2);

const feb = (self => n => febOnly(self(self))(n))
    (self => n => febOnly(self(self))(n));

card(JS)

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

const feb = (function (self) {
    return function (n) {
        return febOnly(self(self))(n);
    }
})(function (self) {
    return function (n) {
        return febOnly(self(self))(n);
    }
});

card(Python+λ)

feb_only = lambda f: lambda n: (
    1 if n <= 2
    else f(n - 1) + f(n - 2)
)

feb = (
    lambda self: lambda n: (
        feb_only(self(self))(n)
    )
)(
    lambda self: lambda n: (
        feb_only(self(self))(n)
    )
)

这样子就方便把 febOnly 提取为参数了:

card(dad_yc)

card(JS+λ)

const febOnly = f => n =>
    n <= 2
        ? 1
        : f(n - 1) + f(n - 2);

const yCombinator = someOnly =>
    (self => n => someOnly(self(self))(n))
    (self => n => someOnly(self(self))(n));

const feb = yCombinator(febOnly);

card(JS)

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

function yCombinator(someOnly) {
    return (function (self) {
        return function (n) {
            return someOnly(self(self))(n);
        }
    })(function (self) {
        return function (n) {
            return someOnly(self(self))(n);
        }
    });
}

const feb = yCombinator(febOnly);

card(Python+λ)

feb_only = lambda f: lambda n: (
    1 if n <= 2
    else f(n - 1) + f(n - 2)
)

y_combinator = lambda some_only: (
    lambda self: lambda n: (
        some_only(self(self))(n)
    )
)(
    lambda self: lambda n: (
        some_only(self(self))(n)
    )
)

feb = y_combinator(feb_only)

card(Python)

这里 Python 可以略过上一步直接提取。

def feb_only(f):
    def calc(n):
        if n <= 2:
            return 1
        else:
            return f(n - 1) + f(n - 2)
    return calc

def y_combinator(some_only):
    def some_with_self(self):
        def do_some(n):
            return some_only(self(self))(n)
        return do_some
    return some_with_self(some_with_self)

feb = y_combinator(feb_only)

没错,到这我们就找到了这个 yCombinator ,大名鼎鼎的 \(Y\) 组合子!

仔细观察上面的 yCombinator ,可以发现你给它传进去任何一个函数 someOnly ,这 someOnly 的第一个参数都能拿到它自己!没错,这就叫不动点了。

我们推导出来的在 lambda 演算里写作:

\[λf.( λs.λn.(f\ (s\ s)\ n)\ λs.λn.(f\ (s\ s)\ n) ) \]

好吧其实这是 \(Z\) 组合子。我们还可以把 \(\lambda n.(f\ (s\ s)\ n)\) 柯里化为 \((f\ (s\ s))\) ,意思都差不多。最终得到的这个才是 \(Y\) 组合子

\[λf.( λs.(f\ (s\ s))\ λs.(f\ (s\ s)) ) \]

但是在急切求值语言(也就是大部分语言)中,柯里化后得到的 \(Y\) 组合子太急切了,会导致栈溢出,所以在急切求值语言中只能用 \(Z\) 组合子。

但是作为一个强迫症,我不想用这么寒碜的 \(Z\) 组合子。所以我发明了用 JS 实现 lambda 演算的库,其中包含了一个真正惰性求值的 \(Y\) 组合子 yC ,感兴趣你可以去用用。

希望这篇文章能给你一个新的视角来看 \(Y\) 组合子。

posted on 2026-03-13 20:16  肉丁土豆表  阅读(45)  评论(0)    收藏  举报