什么是Y组合器?[关闭]


392

Y组合器从事物的“功能”方面讲是计算机科学概念。大多数程序员甚至对组合器一无所知。

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

12
提示,如果您像我一样学习函数式语言,最好离开组合器,直到对它感到适应为止,否则这将是疯狂之路……
Igor Zevaka 2010年

3
对这个问题的编辑者的表情很微笑:)Mads Torgensen博客上的相关链接
Benjol,2011年


1
我写了一个简短的要点,分享了我对Y组合器的理解:gist.github.com/houtianze/b274e4b975a28fe08aee681699c3f7d0我(据我所知)解释了“ Y组合器如何实现递归函数”
ibic,

1
这个问题的范围如何?
宫阪丽

Answers:


201

如果您已准备好长时间阅读,Mike Vanier会提供一个很好的解释。长话短说,它允许您使用不一定本地支持的语言来实现递归。


14
它不过是一个链接而已;这是一个非常简短的摘要的链接。较长的摘要将不胜感激。
马丁·彼得斯

2
这只是一个链接,但没有比这更好的了。这个答案值得(加1票),没有基本情况下可以退出;aka无限递归。
Yavar 2015年

7
@Andre MacFie:我没有对这项工作发表评论,而是对质量发表了评论。通常,有关堆栈溢出的政策是,答案应独立存在,并带有指向更多信息的链接。
约根·

1
@galdre是正确的。这是一个很好的链接,但它只是一个链接。在下面的其他3个答案中也提到了它,但仅作为辅助文档,因为它们本身都是很好的解释。这个答案甚至也不会尝试回答OP的问题。
toraritte

290

Y组合器是一种“功能”(在其他功能上运行的功能),当您不能从自身内部引用该功能时,它可以启用递归。在计算机科学理论中,它概括了递归,抽象了其实现,从而将其与所讨论功能的实际工作区分开。递归函数不需要编译时名称的好处是一种好处。=)

这适用于支持lambda函数的语言。该表达的lambda表达式为基础的性质,通常意味着他们可以不上名字称呼自己。通过声明该变量,对其进行引用,然后为该变量分配lambda来完成自引用循环,可以解决此问题。可以复制lambda变量,然后重新分配原始变量,这会破坏自引用。

Y组合器在静态类型的语言(通常是过程语言)中难以实现,而且经常使用,因为通常类型限制要求在编译时就知道该函数的参数数量。这意味着必须为需要使用的任何参数计数编写一个y-combinator。

以下是在C#中如何使用和使用Y-Combinator的示例。

使用Y组合器涉及构造递归函数的“异常”方式。首先,您必须将函数编写为调用预先存在的函数的代码,而不是本身:

// Factorial, if func does the same thing as this bit of code...
x == 0 ? 1: x * func(x - 1);

然后,将其转换为需要调用一个函数的函数,并返回执行此操作的函数。之所以称其为功能性的,是因为它接受一个功能并对其执行一项操作,从而导致另一功能。

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

现在,您有一个函数,该函数需要一个函数,并返回另一个看起来像阶乘的函数,但它不会调用自身,而是调用传递到外部函数中的参数。您如何使其成为阶乘?将内部函数传递给自身。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.
}

除了阶乘调用本身之外,发生的事情是阶乘调用阶乘生成器(由对Y-Combinator的递归调用返回)。并根据当前的t值,从生成器返回的函数将再次使用t-1调用生成器,或者仅返回1,终止递归。

它是复杂且神秘的,但是在运行时都会彻底消失,其工作的关键是“延迟执行”,以及拆分递归以覆盖两个功能的过程。内部F 作为参数传递仅在必要时在下一次迭代中调用。


5
为什么哦,为什么您必须叫它“ Y”和参数“ F”!他们只是迷失在类型参数中!
Brian Henk

3
在Haskell中,您可以使用:进行抽象递归fix :: (a -> a) -> a,并且acan可以是任意数量的参数函数。这意味着静态类型并不会真正使您麻烦。
Peaker

12
根据Mike Vanier的描述,您对Y的定义实际上不是组合器,因为它是递归的。在“消除(大多数)显式递归(惰性版本)”下,他具有与C#代码等效的惰性方案,但在要点2中进行了解释:“它不是组合器,因为定义主体中的Y是自由变量,因此仅在定义完成后才受约束……”我认为Y组合器的妙处在于它们通过评估函数的定点来产生递归。这样,它们不需要显式递归。
GrantJ 2011年

@GrantJ你说的很对。自从我发布此答案已有两年了。现在略读Vanier的帖子,我发现我写的是Y,但不是Y-Combinator。我将很快再次阅读他的帖子,看看是否可以发布更正。我的直觉警告我,严格的C#静态键入最终可能会阻止它,但是我会看到可以做什么。
克里斯·阿默曼

1
@WayneBurkett这是数学中非常普遍的做法。
YoTengoUnLCD '16

102

我已经从http://www.mail-archive.com/boston-pm@mail.pm.org/msg02716.html取消了此操作,这是我几年前写的一个解释。

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

我们的目标是能够仅使用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
);

现在让我们看看是否可以减少作弊。首先,我们正在使用分配,但是我们不需要。我们可以内联编写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
);

但是我们使用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);

...保持完全相同。(此技巧在其发明者之后被称为“ 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
);

随时尝试。返回的alert(),将其绑定到按钮,无论如何。该代码递归地计算阶乘,而无需使用2个变量的赋值,声明或函数。(但是,尝试跟踪它的工作方式可能会使您的头旋转。如果不对其进行派生,则对其稍加重新格式化就将导致代码肯定令人困惑和混乱。)

您可以将4个递归定义阶乘的行替换为所需的任何其他递归函数。


很好的解释。你为什么写function (n) { return builder(builder)(n);}而不是builder(builder)
v7d8dpo4

@ v7d8dpo4因为我使用currying将2个变量的函数转换为1个变量的高阶函数。
btilly '16

这就是我们需要关闭的原因吗?
TheChetan '17

1
@TheChetan Closures让我们将自定义行为绑定到匿名函数的调用之后。这只是另一种抽象技术。
btilly

85

我不知道尝试从头开始构建它是否有用。让我们来看看。这是一个基本的递归阶乘函数:

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

让我们重构并创建一个名为的新函数fact,该函数返回一个匿名的阶乘计算函数,而不是自己执行计算:

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

var factorial = fact();

有点奇怪,但是没有错。我们只是在每个步骤中生成一个新的阶乘函数。

此阶段的递归仍然相当明确。该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);

很好,但是recurser仍然需要知道自己的名字。我们也将其参数化:

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

var factorial = recurser(recurser);

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

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

var factorial = Y();

现在,我们可以recurser完全取消名称。它只是Y的内部函数的一个参数,可以用函数本身替换:

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

var factorial = Y();

唯一仍引用的外部名称是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);
    };
});

:在JavaScript中类似的解释igstan.ro/posts/...
持久性有机污染物

1
引入该功能时,您迷失了我recurser。丝毫不知道它在做什么,或者为什么。
Mörre

2
我们正在尝试为没有明确递归的功能构建通用的递归解决方案。该recurser函数是朝着这个目标迈出的第一步,因为它为我们提供了一个递归版本,fact该版本从不引用名称。
韦恩2015年

@WayneBurkett,我可以这样重写Y组合器吗function Y(recurse) { return recurse(recurse); } let factorial = Y(creator => value => { return value == 0 ? 1 : value * creator(creator)(value - 1); });?这就是我的消化方式(不确定它是否正确):通过不显式引用该函数(不允许作为组合器),我们可以使用两个部分应用/咖喱函数(一个创建函数和一个calculate函数),我们可以创建无需递归计算函数名称的实现递归的lambda /匿名函数?
neevek '18


24

y-combinator在JavaScript中

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

编辑:通过阅读代码,我学到了很多东西,但是如果没有一些背景知识,很难理解这一点-对此感到抱歉。利用其他答案提供的一些常识,您可以开始区分正在发生的事情。

Y函数是“ y组合器”。现在看一下var factorial使用Y 的那一行。请注意,您将具有参数的函数(在此示例中为recurse)传递给它,该函数稍后还将在内部函数中使用。参数名称基本上成为内部函数的名称,从而允许它执行递归调用(因为它recurse()在其定义中使用。)y组合器执行了将否则为匿名的内部函数与传递给该函数的参数名称相关联的魔力是的

有关Y如何做魔术的完整解释,请查看链接文章(不是我本人)。


6
JavaScript不需要一个Y型组合子做匿名递归,因为你可以访问arguments.callee的当前功能(见en.wikipedia.org/wiki/...
xitrium

6
arguments.callee在严格模式下是不允许的:developer.mozilla.org/en/JavaScript/...
dave1010

2
您仍然可以给任何函数命名,如果是函数表达式,则只能在函数内部知道该名称。(function fact(n){ return n <= 1? 1 : n * fact(n-1); })(5)
Esailija


18

对于尚未深入了解函数式编程并且不关心立即开始但又有些好奇的程序员:

Y组合器是一个公式,可让您在函数没有名称但可以作为参数传递,用作返回值以及在其他函数中定义的情况下实现递归。

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

它是lambda微积分的一部分,它实际上是数学,但实际上是一种编程语言,并且对于计算机科学,尤其是函数式编程非常重要。

Y组合器的日常实用价值受到限制,因为编程语言倾向于让您命名函数。

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

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

通常,您会因为重复而发现它(λx.f (x x))

这些λ符号是希腊字母lambda,它使lambda微积分具有其名称,并且有很多(λx.t)样式术语,因为这就是lambda微积分的外观。


这应该是公认的答案。BTW,带有U x = x xY = U . (. U)(使用类似Haskell的表示法)。IOW,带有适当的组合器,Y = BU(CBU)。因此,Yf = U (f . U) = (f . U) (f . U) = f (U (f . U)) = f ((f . U) (f . U))
内斯

13

匿名递归

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

forall f.  fix f  =  f (fix f)

fix f表示x定点方程的解

               x  =  f x

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

fact 0 = 1
fact n = n * fact (n - 1)

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

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变量的范围,以及betaeta减少规则

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

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

λ x y. E

是的简写

λ x. λ y. E

(抽象多元性),

E F G

是的简写

(E F) G

(应用程序左关联),

λ x. x

λ y. y

是与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
    . . .

正式证明

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

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

λ x. x x

对自己:

   (λ x. x x) (λ y. y y)
=  (λ y. y y) (λ y. y y)
. . .
=  _|_                     Bottom

优先减少最左边的子表达式(“ heads”)。应用顺序在替换之前将参数标准化,而正常顺序则不会。这两种策略类似于渴望评估(例如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 _|_  =  _|_

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

=  (λ l. ((λ i. i) a)) ((λ x. x x) (λ y. y y))
=  (λ l. a) ((λ x. x x) (λ y. y y))
=  a

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

ÿ

定点组合器的基本属性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

(乘法延迟,合流)

对于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))
  . . .

正常顺序的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])


4
我很欣赏答案的彻底性,但是对于第一次换行后几乎没有正规数学背景的程序员来说,这绝对是不可能的。
贾里德·史密斯

4
@ jared-smith答案的目的是讲述Wonkaian关于Y组合器背后的CS /数学概念的补充故事。我认为,其他回答者可能已经对熟悉的概念进行了最好的类比。就我个人而言,我总是喜欢在一个很好的类比之下,面对一个想法的新颖性的真正渊源。我发现最广泛的类比是不合适和令人困惑的。

1
您好,身份组合器λ x . x,您今天好吗?
MaiaVictor '17

喜欢这个答案。它清除了我所有的问题!
学生

11

其他答案对此提供了非常简洁的答案,没有一个重要的事实:您不需要以任何复杂的方式以任何实际语言实现定点组合器,并且这样做没有任何实际目的(“看,我知道什么是Y组合器”是”)。它是重要的理论概念,但实用价值很小。


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

6

Y组合器是磁通电容器的另一个名称。


4
非常有趣。:)年轻人可能无法识别参考。
内斯

2
哈哈!是的,年轻的一个(我)仍然可以理解...


我认为对于非英语使用者来说,这个答案可能会特别令人困惑。在意识到(这是从来没有)它是一种幽默的流行文化参考之前,人们可能会花很多时间来理解这一主张。(我喜欢,如果我回答了这个问题,发现发现有人劝阻它,我会感到
Mike


5

作为新手,我发现了Mike Vanier的文章(感谢Nicholas Mancuso)确实很有帮助。除了记录我的理解之外,我还想写一个摘要,如果对其他人有帮助,我将非常高兴。

胡扯少胡扯

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

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

在上面的伪代码中,almost-factorial采用函数f和数字xalmost-factorial经过咖喱处理,因此可以将其视为采用函数f并返回1-arity函数)。

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

可以看成是阶乘函数almost-factorialcr脚版本(只能计算到number x - 1),并返回阶乘函数的less脚版本(计算到number x)。以此形式:

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

如果我们反复将不那么糟糕的阶乘传递给almost-factorial,最终将获得所需的阶乘函数f。可以认为是:

almost-factorial f = f

定点

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

这是查看上面函数之间关系的一种非常有趣的方式,对我来说这是一个愚蠢的时刻。(如果没有,请阅读Mike关于定点的文章)

三种功能

概括地说,我们有一个非递归函数fn(如我们的几乎阶乘),我们有它的定点函数fr(如我们的f),那么Y当您给出时Y fnY返回的是定点函数fn

因此,总而言之(通过假设fr仅接受一个参数进行简化;递归地x退化为x - 1x - 2...):

  • 我们将核心计算定义为fndef fn fr x = ...accumulate x with result from (fr (- x 1)),这是几乎有用的功能-尽管我们不能fn直接在上使用x,但很快就会有用。这种非递归fn使用函数fr来计算其结果
  • fn fr = frfr是的定点fnfr有用的功能可按,我们可以使用frx,让我们的结果
  • Y fn = frY返回函数的固定点,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)

因此,的确(Y f)是的固定点f

为什么(Y f)起作用?

取决于的签名f(Y f)可以是任何函数的函数,为简化起见,我们假设(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)))

所以

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

对我而言,此设置的神奇之处在于:

  • fnfr相互依赖:每次使用fr“ wrap” 进行计算时,都会“产生”(“ lifts”?)并将计算委托给它(自身和);另一方面,取决于并用于计算较小问题的结果。fnfrxfnfnfrxfnfrfrx-1
  • fr用于定义时fn(在其操作中fn使用fr时),实数fr尚未定义。
  • fn定义了真正的业务逻辑。根据fnY产生fr-在一个特定形式的辅助函数-便于计算为fn递归方式。

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

顺便说一句,我还发现《通过Lambda微积分进行函数式编程入门》一书非常好,我只是其中一部分,而我无法理解Y这本书的事实促使我撰写了这篇文章。


5

以下是原始问题答案,这些答案是根据尼古拉斯·曼库索(Nicholas Mancuso)的答案中提到的文章(总共值得一读)汇编而成 ,以及其他答案:

什么是Y组合器?

Y组合器是一个“函数”(或一个高阶函数-在其他函数上运行的函数),它带有一个参数(该函数不是递归的),并返回该函数的一个版本,即递归的。


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

组合器-只是没有自由变量的lambda表达式。
自由变量-是不是绑定变量的变量。
绑定变量-包含在以该变量名作为其自变量之一的lambda表达式内的变量。

考虑这种情况的另一种方法是,combinator是这样的lambda表达式,您可以在其中找到的位置替换其定义的combinator的名称,并使一切仍然有效(如果combinator可能会陷入无限循环在lambda体内包含对自身的引用)。

Y组合器是定点组合器。

函数的不动点是函数域的元素,该域由函数映射到自身。
也就是说,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))

这意味着,对于给定的函数f(它是非递归函数),可以首先通过计算λx.f(x x),然后将其本身应用于lambda表达式来获得相应的递归函数。

严格(应用顺序)Y组合器:
此定义适用于具有严格(也包括:渴望,贪婪)评估的语言-一种评估策略,在该策略中,表达式一旦绑定到变量就立即进行评估。

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

它本质上与懒惰的人相同,只是有一个额外的λ包装来延迟对lambda的身体评估。我问了另一个问题,与这个话题有些相关。

它们有什么用?

克里斯·安默曼(Chris Ammerman)答案中借来了Stolen:Y-combinator概括了递归,将其实现抽象化,从而将其与相关功能的实际工作区分开。

尽管Y-combinator有一些实际应用,但它主要是一个理论概念,对它的理解将扩大您的总体视野,并有可能增加您的分析和开发技能。

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

正如Mike Vanier所说可以用许多静态类型的语言定义Y组合器,但是(至少在我所看到的示例中)这样的定义通常需要一些非显而易见的类型的黑客,因为Y组合器本身不需要具有简单的静态类型。这超出了本文的范围,因此我不再赘述

就像克里斯·阿默曼(Chris Ammerman)提到的那样:大多数程序语言都具有静态类型。

因此,请回答这个问题-并非如此。


4

y-combinator实现匿名递归。所以代替

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

你可以做

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

当然,y-combinator仅适用于按名字呼叫的语言。如果要在任何普通的按值调用语言中使用此功能,则需要相关的z组合器(y组合器将发散/无限循环)。


Y组合器可以处理传递值和惰性评估。
Quelklef '19

3

定点组合器(或定点运算符)是计算其他函数的定点的高阶函数。此操作与编程语言理论有关,因为它允许重写规则形式的递归实现,而无需语言运行时引擎的明确支持。(src维基百科)


3

该操作员可以简化您的生活:

var Y = function(f) {
    return (function(g) {
        return g(g);
    })(function(h) {
        return function() {
            return f.apply(h(h), arguments);
        };
    });
};

然后,避免使用额外的功能:

var fac = Y(function(n) {
    return n == 0 ? 1 : n * this(n - 1);
});

最后,您致电fac(5)


0

我认为回答这个问题的最好方法是选择一种语言,例如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-combinator解决的问题。


1
您确定它不会产生超出其解决范围的问题吗?
Noctis Skytower

1
Noctis,您能澄清您的问题吗?您是在问y-combinator的概念本身是否会解决更多的问题,还是在专门谈论我选择使用JavaScript进行演示,还是我的具体实现或通过发现自己的建议来学习它?我描述了吗?
zumalifeguard '16
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.