从“函数”的角度来看,y组合子是一个计算机科学的概念。大多数程序员根本不了解组合子,如果他们听说过的话。

什么是y组合子? 组合符是如何工作的? 它们有什么用? 它们在过程语言中有用吗?


当前回答

我认为回答这个问题的最好方法是选择一种语言,比如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组合子解决了什么问题。

其他回答

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

从糟糕到不那么糟糕

以factorial为例,我们使用下面的almost-factorial函数来计算number x的阶乘:

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

在上面的伪代码中,almost-阶乘接受函数f和数字x (almost-阶乘是curry的,所以它可以被视为接受函数f并返回一个1-arity函数)。

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

它可以被看作是almost-阶乘接受了一个蹩脚的阶乘函数(它只能计算到数字x - 1),并返回一个不那么蹩脚的阶乘(计算到数字x)。如下形式:

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

如果我们反复地将阶乘的不那么糟糕的版本传递给almost阶乘,我们最终会得到我们想要的阶乘函数f。其中可以考虑为:

almost-factorial f = f

Fix-point

几乎阶乘f = f意味着f是几乎阶乘函数的定点。

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

三个函数

概括地说,我们有一个非递归函数fn(就像我们的几乎阶乘),我们有它的定点函数fr(就像我们的f)然后Y所做的是当你给Y fn, Y返回fn的定点函数。

总之(通过假设fr只有一个参数来简化;X退化为X - 1, X - 2…在递归):

我们将核心计算定义为fn: def fn fr x =…将x与result from (fr (- x1))累加,这是一个几乎有用的函数-尽管我们不能直接在x上使用fn,但它很快就会有用。这个非递归fn使用一个函数fr来计算它的结果 fnfr = fr, fr是Fn的定点,fr是有用的函数,我们可以用fr作用于x来得到结果 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)

所以(yf)的结果就是f的定点。

(yf)为什么成立?

根据f的特征,(Y f)可以是任意arity的函数,为了简化,我们假设(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)))

So

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

对我来说,这种设置的神奇之处在于:

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.

它帮助我现在这样理解Y,希望有帮助。

顺便说一句,我还发现《通过Lambda微积分介绍函数式编程》这本书非常好,我只读了一部分,事实上,我无法理解书中的Y,这让我想到了这篇文章。

为了帮助自己掌握Y-Combinator,我在Clojure和Scheme中都写了一份关于Y-Combinator的“傻瓜指南”。他们受到了《小阴谋家》的影响

在方案: https://gist.github.com/z5h/238891

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

这两个教程都是代码中穿插的注释,应该剪切和粘贴到您最喜欢的编辑器。

匿名的递归

定点组合子是一种根据定义满足等价的高阶函数固定

forall f.  fix f  =  f (fix f)

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

               x  =  f x

自然数的阶乘可以用

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演算形式主义包含在上下文无关的语法中

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

v在变量范围内,和约简规则一起

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

Beta约简用表达式(“参数”)e替换抽象(“函数”)体B中变量x的所有自由出现。Eta约简消除了冗余抽象。它有时在形式主义中被省略。不适用约简规则的不可约表达式是正常形式或规范形式。

λ x y. E

是简写

λ x. λ y. E

(抽象multiarity),

E F G

是简写

(E F) G

(应用程序left-associativity),

λ x. x

and

λ y. y

alpha-equivalent。

抽象和应用是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微积分中,组合子是不包含自由变量的抽象。最简单的,I,单位组合子

λ x. x

同构的恒等函数

id x = x

这样的组合子是像SKI系统这样的组合子计算器的基本操作符。

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

减少不是强归一化;并不是所有的可约化表达式,“重解”,在约简下收敛到正常形式。一个简单的例子是ω ω组合子的发散应用

λ 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 _|_  =  _|_

但在惰性法阶约简下是收敛的

=  (λ 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])

大卫·特纳:丘奇的论文和函数式编程 阿朗佐·丘奇:初等数论的一个不可解问题 微积分 Church-Rosser定理

我从http://www.mail-archive.com/boston-pm@mail.pm.org/msg02716.html中引用了这个,这是我几年前写的一个解释。

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

我们的目标是写出一个1的递归函数 变量只使用1变量的函数,没有 赋值,通过名称定义事物等(为什么这是我们的 目标是另一个问题,我们把它作为 我们所面临的挑战。)似乎不可能,是吧?作为 举个例子,让我们实现阶乘。

第一步是说我们可以很容易地做到这一点,如果我们 作弊了一点。使用二元函数和 我们至少可以避免使用 赋值来建立递归。

// 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
);

但是我们用两个变量的函数来得到一个1的函数 变量。我们能解决这个问题吗?一个叫 Haskell Curry有一个巧妙的技巧,如果你有好的高阶 那么你只需要一个变量的函数。的 证明是你可以从函数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行替换为 任何你想要的递归函数。

Y-Combinator是通量电容器的另一个名称。