什么是Y-combinator?

Chr*_*man 381 theory computer-science functional-programming combinators definition

Y-combinator是一种来自事物"功能"方面的计算机科学概念.大多数程序员对组合器一无所知,如果他们甚至听说过它们的话.

  • 什么是Y-combinator?
  • 组合器如何工作?
  • 它们有什么用?
  • 它们在程序语言中有用吗?

Chr*_*man 285

Y-combinator是一个"功能"(对其他函数起作用的函数),当你无法从内部引用函数时,它会启用递归.在计算机科学理论中,它概括了递归,抽象了它的实现,从而将它与所讨论的函数的实际工作分开.不需要递归函数的编译时名称的好处是一种奖励.=)

这适用于支持lambda函数的语言.基于表达的lambdas本质通常意味着他们不能通过名字来引用自己.通过声明变量,引用它,然后将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);
Run Code Online (Sandbox Code Playgroud)

然后将其转换为一个函数,它接受一个函数来调用,并返回一个执行此操作的函数.这称为功能,因为它需要一个函数,并执行一个操作,导致另一个函数.

// 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);
Run Code Online (Sandbox Code Playgroud)

现在你有一个函数,它的功能,并返回另一个功能之类的,看起来像一个因子,但不是自称,它调用传递到外部函数的参数.你如何使这成为阶乘?将内部函数传递给自己.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.
}
Run Code Online (Sandbox Code Playgroud)

而不是因子调用本身,发生的是因子调用阶乘生成器(由递归调用Y-Combinator返回).并且根据t的当前值,从生成器返回的函数将再次调用生成器,使用t - 1,或者只返回1,终止递归.

它既复杂又神秘,但它在运行时都会抖动,其工作的关键是"延迟执行",以及分解两个函数的递归.内部F 作为参数传递,仅在必要时才在下一次迭代中调用.

  • 根据Mike Vanier的描述,你对Y的定义实际上不是**组合,因为它是递归的.在"消除(大多数)显式递归(懒惰版本)"下,他的懒惰方案等同于你的C#代码,但在第2点解释:"它不是组合子,因为定义体中的Y是一个自由变量,只有在定义完成后才会绑定..."我认为Y组合器的一个很酷的事情是它们通过评估函数的定点来产生递归.这样,它们就不需要显式递归. (12认同)
  • 为什么哦为什么要把它称为'Y'和参数'F'!他们只是迷失在类型参数中! (5认同)
  • 在Haskell中,你可以使用:`fix ::(a - > a) - > a`来抽象递归,而`a`又可以是你想要的参数的函数.这意味着静态类型并不能真正使这个麻烦. (3认同)

Nic*_*uso 195

如果你准备好长时间阅读,Mike Vanier有一个很好的解释.简而言之,它允许您使用一种本身不一定支持它的语言来实现递归.

  • 考虑到其他答案的质量,这不应该是公认的答案. (14认同)
  • 它虽然略多于一个链接; 它是一个*非常简短的摘要*的链接.我们将不胜感激. (13认同)
  • @Andre MacFie:我没有评论这项努力,我评论了质量.一般来说,Stack Overflow上的策略是答案应该是自包含的,并带有更多信息的链接. (7认同)
  • 这只是一个链接,但没有比这更好的了。这个答案值得(加1票),没有基本情况下可以退出;aka无限递归。 (2认同)

bti*_*lly 101

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

我将在此示例中使用JavaScript,但许多其他语言也可以使用.

我们的目标是能够仅使用1个变量的函数编写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
);
Run Code Online (Sandbox Code Playgroud)

现在让我们看看我们是否可以减少欺骗.首先,我们正在使用任务,但我们不需要.我们可以直接写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
);
Run Code Online (Sandbox Code Playgroud)

但是我们使用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);
Run Code Online (Sandbox Code Playgroud)

哪里......保持完全相同.(这个技巧在其发明者之后被称为"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
);
Run Code Online (Sandbox Code Playgroud)

随意试试吧.alert()返回,将它绑定到一个按钮,无论如何.该代码以递归方式计算阶乘,而不使用2个变量的赋值,声明或函数.(但是试图追踪它是如何工作的可能会使你的头部旋转.并且在没有推导的情况下处理它,只是稍微重新格式化将导致代码肯定会让人感到困惑和混淆.)

您可以使用所需的任何其他递归函数替换递归定义阶乘的4行.


Way*_*ett 85

我想知道从头开始构建这个是否有任何用处.让我们来看看.这是一个基本的递归因子函数:

function factorial(n) {
    return n == 0 ? 1 : n * factorial(n - 1);
}
Run Code Online (Sandbox Code Playgroud)

让我们重构并创建一个名为的新函数fact,它返回一个匿名析因计算函数,而不是执行计算本身:

function fact() {
    return function(n) {
        return n == 0 ? 1 : n * fact()(n - 1);
    };
}

var factorial = fact();
Run Code Online (Sandbox Code Playgroud)

这有点奇怪,但它并没有错.我们只是在每一步产生一个新的阶乘函数.

此阶段的递归仍然相当明确.该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);
Run Code Online (Sandbox Code Playgroud)

这很好,但recurser仍需要知道自己的名字.我们也要参数化:

function recurser(f) {
    return fact(function(x) {
        return f(f)(x);
    });
}

var factorial = recurser(recurser);
Run Code Online (Sandbox Code Playgroud)

现在,recurser(recurser)让我们创建一个返回其结果的包装函数,而不是直接调用:

function Y() {
    return (function(f) {
        return f(f);
    })(recurser);
}

var factorial = Y();
Run Code Online (Sandbox Code Playgroud)

我们现在可以recurser完全摆脱这个名字; 它只是Y的内部函数的一个参数,可以用函数本身代替:

function Y() {
    return (function(f) {
        return f(f);
    })(function(f) {
        return fact(function(x) {
            return f(f)(x);
        });
    });
}

var factorial = Y();
Run Code Online (Sandbox Code Playgroud)

唯一仍然引用的外部名称是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);
    };
});
Run Code Online (Sandbox Code Playgroud)

  • 应该开始:返回n <= 2?(n == 2?2:1):n*阶乘(n-1); 正确答案0-> 1 :).无论如何,很高兴看到进化. (4认同)
  • 我们正在尝试为没有明确递归的功能构建通用的递归解决方案。`recurser`函数是朝着这个目标迈出的第一步,因为它为我们提供了'fact'的递归版本,该版本从不引用名称。 (2认同)

Jør*_*ogh 50

上面的大多数答案都描述了Y-combinator 什么,但不是它的用途.

固定点组合器用于表明lambda演算完整的.这是计算理论中非常重要的结果,为函数式编程提供了理论基础.

学习定点组合器也帮助我真正理解函数式编程.我在实际编程中从未发现它们有任何用处.


Zac*_*ach 24

JavaScript中的 y-combinator :

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
Run Code Online (Sandbox Code Playgroud)

编辑:我从查看代码中学到了很多东西,但是如果没有一些背景知识,这一点有点难以接受 - 抱歉.通过其他答案提供的一些常识,您可以开始挑选正在发生的事情.

Y函数是"y组合子".现在看一下var factorial使用Y 的行.请注意,您将一个函数传递给它,该函数具有一个参数(在此示例中recurse),该函数稍后也会在内部函数中使用.参数名称基本上成为内部函数的名称,允许它执行递归调用(因为它使用recurse()了它的定义.)y-combinator执行将其他匿名内部函数与传递给函数的函数的参数名称相关联的魔力. Y.

有关Y如何完成魔法的完整解释,请查看链接的文章(不是我的btw).

  • Javascript不需要Y-combinator来进行匿名递归,因为您可以使用arguments.callee访问当前函数(请参阅http://en.wikipedia.org/wiki/Fixed_point_combinator#Anonymous_recursion_by_other_means) (6认同)
  • 在严格模式下不允许使用`arguments.callee`:https://developer.mozilla.org/en/JavaScript/Strict_mode#Making_eval_and_arguments_simpler (5认同)
  • 您仍然可以为任何函数指定名称,如果它是函数表达式,则该名称仅在函数本身内部已知.`(函数fact(n){return n <= 1?1:n*fact(n-1);})(5)` (2认同)

El *_*rko 17

对于那些没有深入体验过函数式编程的程序员而言,现在并不关心,但是有点好奇:

Y组合子是一个公式,它允许您在函数不能具有名称但可以作为参数传递,用作返回值并在其他函数中定义的情况下实现递归.

它通过将函数作为参数传递给自身来工作,因此它可以调用自身.

它是lambda演算的一部分,它实际上是数学,但实际上是一种编程语言,对于计算机科学尤其是函数式编程来说是非常基础的.

Y组合器的日常实用价值是有限的,因为编程语言倾向于让您命名功能.

如果您需要在警察阵容中识别它,它看起来像这样:

Y =λf.(λx.f(xx))(λx.f(xx))

你可以经常发现它,因为重复(?x.f (x x)).

?符号是希腊字母拉姆达,这给演算它的名字,并且有很多的(?x.t)风格上,因为这是演算的样子.


小智 11

其他答案提供了非常简洁的答案,没有一个重要的事实:你不需要用这种复杂的方式用任何实用语言实现定点组合器,这样做没有实际意义(除了"看,我知道Y-combinator是什么)是").这是重要的理论概念,但没有什么实际价值.


小智 7

匿名递归

定点组合器是fix根据定义满足等价关系的高阶函数

forall f.  fix f  =  f (fix f)
Run Code Online (Sandbox Code Playgroud)

fix f表示x定点方程的解

               x  =  f x
Run Code Online (Sandbox Code Playgroud)

自然数的阶乘可以通过以下方式证明

fact 0 = 1
fact n = n * fact (n - 1)
Run Code Online (Sandbox Code Playgroud)

使用fix,可以得出关于通用/?递归函数的任意构造证明,而不会产生非对称的自我指称性。

fact n = (fix fact') n
Run Code Online (Sandbox Code Playgroud)

哪里

fact' rec n = if n == 0
                then 1
                else n * rec (n - 1)
Run Code Online (Sandbox Code Playgroud)

这样

   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
Run Code Online (Sandbox Code Playgroud)

这种形式上的证明

fact 3  =  6
Run Code Online (Sandbox Code Playgroud)

有条不紊地使用定点组合器等效项进行重写

fix fact'  ->  fact' (fix fact')
Run Code Online (Sandbox Code Playgroud)

λ演算

类型化的lambda演算形式主义在于上下文无关的语法

E ::= v        Variable
   |  ? v. E   Abstraction
   |  E E      Application
Run Code Online (Sandbox Code Playgroud)

其中v变量的范围,以及betaeta减少规则

(? x. B) E  ->  B[x := E]                                 Beta
  ? x. E x  ->  E          if x doesn’t occur free in E   Eta
Run Code Online (Sandbox Code Playgroud)

Beta减少将表达式(“参数”)替换x为抽象(“函数”)主体中变量的所有自由出现。减少Eta消除了多余的抽象。有时从形式主义中将其省略。没有归约规则的不可归约表达式为正则规范形式BE

? x y. E
Run Code Online (Sandbox Code Playgroud)

是的简写

? x. ? y. E
Run Code Online (Sandbox Code Playgroud)

(抽象多元性),

E F G
Run Code Online (Sandbox Code Playgroud)

是的简写

(E F) G
Run Code Online (Sandbox Code Playgroud)

(应用程序左关联),

? x. x
Run Code Online (Sandbox Code Playgroud)

? y. y
Run Code Online (Sandbox Code Playgroud)

是与alpha等效的

抽象和应用是lambda演算的两个唯一的“语言原语”,但是它们允许对任意复杂的数据和操作进行编码

教堂数字是自然数的编码,类似于Peano-axiomatic自然数。

   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
    . . .
Run Code Online (Sandbox Code Playgroud)

正式证明

1 + 2  =  3
Run Code Online (Sandbox Code Playgroud)

使用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
Run Code Online (Sandbox Code Playgroud)

组合器

在lambda演算中,组合器是不包含自由变量的抽象。最简单的:I身份组合器

? x. x
Run Code Online (Sandbox Code Playgroud)

身份函数同构

id x = x
Run Code Online (Sandbox Code Playgroud)

这样的组合器是像SKI系统一样的组合器结石的原始运算符。

S  =  ? x y z. x z (y z)
K  =  ? x y. x
I  =  ? x. x
Run Code Online (Sandbox Code Playgroud)

Beta的减少不是很正常的;并非所有可还原的表达式“ redexes”在beta还原下都收敛为正常形式。一个简单的例子是omega ?组合器的不同应用

? x. x x
Run Code Online (Sandbox Code Playgroud)

对自己:

   (? x. x x) (? y. y y)
=  (? y. y y) (? y. y y)
. . .
=  _|_                     Bottom
Run Code Online (Sandbox Code Playgroud)

优先考虑最左边子表达式(“ heads”)的减少。应用顺序在替换之前将参数标准化,而正常顺序则不会。这两种策略类似于热切评估(例如C)和惰性评估(例如Haskell)。

   K          (I a)        (? ?)
=  (? k l. k) ((? i. i) a) ((? x. x x) (? y. y y))
Run Code Online (Sandbox Code Playgroud)

在急切的应用顺序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))
. . .
=  _|_
Run Code Online (Sandbox Code Playgroud)

因为严格的语义

forall f.  f _|_  =  _|_
Run Code Online (Sandbox Code Playgroud)

但收敛于懒惰的正序beta减少

=  (? l. ((? i. i) a)) ((? x. x x) (? y. y y))
=  (? l. a) ((? x. x x) (? y. y y))
=  a
Run Code Online (Sandbox Code Playgroud)

如果表达式具有正常形式,则可以找到正常顺序的beta减少形式。

ÿ

定点组合器的基本属性Y

? f. (? x. f (x x)) (? x. f (x x))
Run Code Online (Sandbox Code Playgroud)

是(谁)给的

   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))
. . .                                      . . .
Run Code Online (Sandbox Code Playgroud)

等价

Y g  =  g (Y g)
Run Code Online (Sandbox Code Playgroud)

同构

fix f  =  f (fix f)
Run Code Online (Sandbox Code Playgroud)

未类型化的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
Run Code Online (Sandbox Code Playgroud)

(乘法延迟,合流)

对于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))
  . . .
Run Code Online (Sandbox Code Playgroud)

正常顺序的beta减少使未扩展的未类型化lambda演算成为图灵完全重写系统。

在Haskell中,定点组合器可以轻松实现

forall f.  fix f  =  f (fix f)
Run Code Online (Sandbox Code Playgroud)

在评估所有子表达式之前,Haskell的懒惰会归一化为无穷大。

               x  =  f x
Run Code Online (Sandbox Code Playgroud)

  • @ jared-smith答案的目的是讲述Wonkaian有关Y组合器背后的CS /数学概念的补充故事。我认为,可能其他回答者已经对熟悉的概念做出了最好的类比。就我个人而言,我一直喜欢面对一个想法的真正根源,即[激进的新颖性](https://www.cs.utexas.edu/~EWD/transcriptions/EWD10xx/EWD1036.html),比喻。我发现最广泛的类比是不合适和令人困惑的。 (4认同)
  • 我很欣赏答案的彻底性,但是对于第一次换行后几乎没有正规数学背景的程序员来说,这绝对是不可能的。 (2认同)

xgM*_*gMz 6

这是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);
Run Code Online (Sandbox Code Playgroud)


Jon*_*vis 6

Y-Combinator是磁通电容器的另一个名称.

  • 非常有趣.:)年轻(呃)可能不认识参考. (4认同)
  • 哈哈!是的,年轻人(我)仍然可以理解...... (2认同)

z5h*_*z5h 5

我在Clojure和Scheme中为Y-Combinator写了一篇"白痴指南",以帮助自己掌握它.他们受到"The Little Schemer"中材料的影响

在Scheme中:https: //gist.github.com/z5h/238891

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

这两个教程都是散布着注释的代码,应该剪切并粘贴到您喜欢的编辑器中.


Dap*_* Li 5

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

蹩脚不蹩脚

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

def almost-factorial f x = if iszero x
                           then 1
                           else * x (f (- x 1))
Run Code Online (Sandbox Code Playgroud)

在上面的伪代码中,传入almost-factorial函数f和数字xalmost-factorial是柯里化的,因此可以看作是传入函数f并返回一个1元函数)。

almost-factorial计算阶乘 for 时x,它将阶乘 for的计算委托x - 1给函数f并累加该结果x(在这种情况下,它将 (x - 1) 的结果乘以 x)。

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

almost-factorial crappy-f = less-crappy-f
Run Code Online (Sandbox Code Playgroud)

如果我们反复将不太糟糕的阶乘版本传递给almost-factorial,我们最终会得到我们想要的阶乘函数f。它可以被认为是:

almost-factorial f = f
Run Code Online (Sandbox Code Playgroud)

定点

这事实上almost-factorial f = f意味着f定点的功能almost-factorial

这是查看上述函数关系的一种非常有趣的方式,对我来说是一个啊哈时刻。(如果你还没有,请阅读 Mike 的关于 fix-point 的帖子)

三大功能

概括地说,我们有一个非递归函数fn(比如我们的几乎阶乘),我们有它的定点函数fr(比如我们的 f),那么Y当你给出 时Y fnY返回 的定点函数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有用的函数,我们可以使用fronx来得到我们的结果
  • 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))
Run Code Online (Sandbox Code Playgroud)

如果我们替换s函数左边的变量,我们得到

Y f = ?s.(f (s s)) ?s.(f (s s))
=> f (?s.(f (s s)) ?s.(f (s s)))
=> f (Y f)
Run Code Online (Sandbox Code Playgroud)

所以确实, 的结果(Y f)是 的不动点f

为什么(Y f)有效?

根据签名f(Y f)可以是任何元数的功能,以简化,假设(Y f)只需要一个参数,就像我们的阶乘函数。

def fn fr x = accumulate x (fr (- x 1))
Run Code Online (Sandbox Code Playgroud)

因为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)))
Run Code Online (Sandbox Code Playgroud)

当最内层(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)))
Run Code Online (Sandbox Code Playgroud)

所以

fr x = Y fn x = fn (?s.(fn (s s)) ?s.(fn (s s))) x
Run Code Online (Sandbox Code Playgroud)

对我来说,这个设置的神奇部分是:

  • fn并且fr相互依赖:fr“包裹”fn在里面,每次fr都用于计算x,它“产生”(“提升”?)fn并将计算委托给那个fn(传递本身frx);另一方面,fn取决于fr并用于fr计算较小问题的结果x-1
  • 当时fr是用来定义的fn(在其运算中fn使用fr时),真正fr的还没有定义。
  • fn定义了真正的业务逻辑。根据fnY产生fr-在一个特定形式的辅助函数-便于计算为fn递归方式。

目前它帮助我以Y这种方式理解,希望它有所帮助。

顺便说一句,我还发现通过 Lambda 微积分介绍函数式编程这本书非常好,我只是其中的一部分,而且我无法Y在书中理解这一事实使我找到了这篇文章。


小智 5

以下是从Nicholas Mancuso回答中提到的文章(完全值得一读)汇编 的原始问题答案,以及其他答案:

什么是 Y 组合子?

Y 组合器是一个“函数”(或一个高阶函数——一个对其他函数进行运算的函数),它接受一个参数,它是一个非递归函数,并返回函数的一个版本,它是递归的。


有点递归=),但更深入的定义:

组合子——只是一个没有自由变量的 lambda 表达式。
自由变量——是一个非绑定变量的变量。
绑定变量 — 包含在 lambda 表达式主体内的变量,该表达式将该变量名称作为其参数之一。

另一种思考方式是组合子就是这样一个 lambda 表达式,在其中您可以在任何找到组合子的地方用它的定义替换组合子的名称,并且一切仍然有效(如果组合子会,您将进入无限循环在 lambda 体内包含对自身的引用)。

Y-combinator 是一个定点组合器。

函数的不动点是函数域的一个元素,它被函数映射到自身。
也就是说,c是函数的一个不动点,f(x)如果f(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))
Run Code Online (Sandbox Code Playgroud)

这意味着,对于给定的函数f(它是一个非递归函数),可以先通过计算得到对应的递归函数?x.f(x x),然后将这个 lambda 表达式应用到自身。

Strict (applicative-order) Y-combinator:
这个定义适用于具有严格(也:急切、贪婪)求值的语言——表达式在绑定到变量后立即求值的求值策略。

Y = ?f.(?x.f(?y.((x x) y))) (?x.f(?y.((x x) y))) = ?f.(?x.(x x)) (?x.f(?y.((x x) y)))
Run Code Online (Sandbox Code Playgroud)

它在本质上与惰性相同,只是有一个额外的?包装器来延迟 lambda 的主体评估。我问了另一个问题,与此主题有些相关。

它们有什么用?

StolenChris Ammerman 的回答中借用了:Y-combinator 概括了递归,抽象了它的实现,从而将它与所讨论函数的实际工作分开。

尽管 Y-combinator 有一些实际应用,但它主要是一个理论概念,理解它会扩展您的整体视野,并可能提高您的分析和开发人员技能。

它们在过程语言中有用吗?

正如Mike Vanier 所说可以在许多静态类型语言中定义 Y 组合子,但是(至少在我见过的示例中)这样的定义通常需要一些不明显的类型技巧,因为 Y 组合子本身没有t 有一个简单的静态类型。这超出了本文的范围,所以我不再赘述

正如Chris Ammerman提到的:大多数过程语言都有静态类型。

所以回答这个 - 不是真的。