ChatGPT解决这个技术问题 Extra ChatGPT

关闭。这个问题需要更加集中。它目前不接受答案。想改进这个问题?更新问题,使其仅通过编辑此帖子专注于一个问题。 3年前关闭。改进这个问题

组合器是从事物的“功能”方面来看的计算机科学概念。大多数程序员对组合子一无所知,即使他们听说过它们。

什么是 Y 组合器?

组合器如何工作?

它们有什么用?

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

一点提示,如果你像我一样学习函数式语言,最好离开组合器直到你适应它,否则这是一条疯狂的道路......
不得不对这个问题的编辑者的头像微笑:) Related link on Mads Torgensen's blog
我写了一个简短的要点来分享我对 Y Combinator 的理解:gist.github.com/houtianze/b274e4b975a28fe08aee681699c3f7d0 我解释了(据我所知)“Y Combinator 如何实现递归函数”
这个问题怎么“太宽泛”了?

t
tanascius

Y-combinator 是一种“函数式”(一种对其他函数进行操作的函数),当您无法从其内部引用该函数时,它可以实现递归。在计算机科学理论中,它概括了递归,抽象了它的实现,从而将它与所讨论函数的实际工作区分开来。递归函数不需要编译时名称的好处是一种奖励。 =)

这适用于支持 lambda functions 的语言。 lambda 基于 expression 的性质通常意味着它们不能通过名称引用自己。通过声明变量、引用它、然后将 lambda 分配给它来完成自引用循环来解决这个问题是很脆弱的。可以复制 lambda 变量,并重新分配原始变量,这会破坏自引用。

static-typed 语言(procedural languages 通常是这样)中,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);

然后你把它变成一个函数,它需要一个函数来调用,并返回一个这样做的函数。这被称为函数,因为它接受一个函数,并用它执行一个操作,从而产生另一个函数。

// 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 作为参数传递,仅在必要时在下一次迭代中调用。


为什么哦为什么你必须称它为'Y'和参数'F'!他们只是迷失在类型参数中!
在 Haskell 中,您可以使用 fix :: (a -> a) -> a 抽象递归,而 a 又可以是您想要的任意数量参数的函数。这意味着静态类型并没有真正使这变得麻烦。
根据 Mike Vanier 的描述,您对 Y 的定义实际上不是组合子,因为它是递归的。在“消除(大多数)显式递归(惰性版本)”下,他具有与您的 C# 代码等效的惰性方案,但在第 2 点中解释说:“它不是组合子,因为定义主体中的 Y 是一个自由变量,它仅在定义完成后才绑定......”我认为 Y 组合器的酷之处在于它们通过评估函数的不动点来产生递归。通过这种方式,它们不需要显式递归。
@GrantJ你说得很好。自从我发布这个答案以来已经有几年了。现在浏览 Vanier 的帖子,我看到我写的是 Y,但不是 Y-Combinator。我很快会再次阅读他的帖子,看看我是否可以发布更正。我的直觉警告我,C# 的严格静态类型最终可能会阻止它,但我会看看我能做什么。
@WayneBurkett 这是数学中非常普遍的做法。
I
I. J. Kennedy

如果您已准备好进行长篇阅读,Mike Vanier has a great explanation。长话短说,它允许您用一种不一定支持本机的语言来实现递归。


不过,它不仅仅是一个链接;这是一个非常简短摘要的链接。一个更长的总结将不胜感激。
这只是一个链接,但它不能比这更好。这个答案值得(add1 票),没有基本情况条件退出;又名无限递归。
@Andre MacFie:我没有评论努力,我评论了质量。一般来说,堆栈溢出的政策是答案应该是自包含的,并带有指向更多信息的链接。
@galdre 是对的。这是一个很好的链接,但它只是一个链接。在下面的其他 3 个答案中也提到了它,但仅作为支持文件,因为它们本身都有很好的解释。这个答案甚至也没有尝试回答 OP 的问题。
M
Michael Myers

我从 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 因为我正在使用 currying 将 2 个变量的函数转换为一个变量的高阶函数。
这是我们需要闭包的原因吗?
@TheChetan 闭包让我们将自定义行为绑定到匿名函数的调用后面。它只是另一种抽象技术。
@LearningMathematics 声明函数实际上是一种赋值形式。但在大多数语言中,它位于不同的命名空间中。但在 JavaScript 中却不是这样,您甚至可以用赋值替换已声明的函数!
W
Wayne

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

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/…
当您介绍函数 recurser 时,您失去了我。一点也不知道它在做什么,或者为什么。
我们正在尝试为非显式递归的函数构建通用递归解决方案。 recurser 函数是实现这一目标的第一步,因为它为我们提供了 fact 的递归版本,它从不通过名称引用自身。
@WayneBurkett,我可以像这样重写 Y 组合器吗:function Y(recurse) { return recurse(recurse); } let factorial = Y(creator => value => { return value == 0 ? 1 : value * creator(creator)(value - 1); });。这就是我消化它的方式(不确定它是否正确):通过不显式引用函数(不允许作为 combinator),我们可以使用两个 partially applied/curried 函数(创建函数和计算函数),我们可以使用它创建实现递归的 lambda/匿名函数,而无需计算函数的名称?
J
Jørgen Fogh

上面的大多数答案都描述了 Y 组合器的用途,但没有描述它的用途。

Fixed point combinators 用于表明 lambda calculusturing complete。这是计算理论中一个非常重要的结果,为functional programming提供了理论基础。

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


E
El Zorko

对于没有深入接触过函数式编程的程序员,也不想从现在开始,但有点好奇:

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

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

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

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

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

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

由于重复的 (λx.f (x x)),您通常可以发现它。

λ 符号是希腊字母 lambda,它给出了 lambda 演算的名称,并且有很多 (λx.t) 样式术语,因为这就是 lambda 演算的样子。


这应该是公认的答案。顺便说一句,使用 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))
M
Michael Myers

JavaScript 中的 y 组合器:

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 组合器”。现在看一下使用 Y 的 var factorial 行。请注意,您将一个函数传递给它,该函数有一个参数(在本例中为 recurse),该参数稍后也会在内部函数中使用。参数名称基本上成为内部函数的名称,允许它执行递归调用(因为它在其定义中使用 recurse()。)y 组合器执行将其他匿名内部函数与参数名称相关联的魔力传递给 Y 的函数。

有关 Y 如何发挥魔力的完整说明,请查看 linked article(顺便说一句,不是我。)


Javascript 不需要 Y 组合器来执行匿名递归,因为您可以使用 arguments.callee 访问当前函数(请参阅 en.wikipedia.org/wiki/…
严格模式下不允许使用 arguments.calleedeveloper.mozilla.org/en/JavaScript/…
您仍然可以为任何函数命名,如果它是函数表达式,则该名称仅在函数本身内部是已知的。 (function fact(n){ return n <= 1? 1 : n * fact(n-1); })(5)
除了在 IE 中。 kangax.github.io/nfe
佚名

匿名递归

定点组合器是一个高阶函数 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演算

无类型 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 减少将抽象(“函数”)主体 B 中变量 x 的所有自由出现替换为表达式(“参数”)E。 Eta 减少消除了冗余抽象。它有时会从形式主义中省略。一个 irreducible 表达式(不适用任何归约规则)是 normalcanonical 形式

λ x y. E

是简写

λ x. λ y. E

(抽象多元性),

E F G

是简写

(E F) G

(应用程序左关联性),

λ x. x

λ y. y

是 alpha 等效的。

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

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

   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 演算中,combinators 是不包含自由变量的抽象。最简单的: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

优先减少最左边的子表达式(“头”)。应用顺序在替换之前规范化参数,正常顺序不会。这两种策略类似于急切求值(例如 C)和惰性求值(例如 Haskell)。

   K          (I a)        (ω ω)
=  (λ k l. k) ((λ i. i) a) ((λ x. x x) (λ y. y y))

在急切的应用阶贝塔减少下发散

=  (λ 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 定点组合子的基本属性

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

David Turner:Church 的论文和函数式编程

Alonzo Church:初等数论的一个无法解决的问题

Lambda演算

Church-Rosser 定理


虽然我很欣赏答案的彻底性,但对于在第一个换行符之后几乎没有正式数学背景的程序员来说,这绝不是平易近人的。
@jared-smith 答案旨在讲述关于 Y 组合器背后的 CS/数学概念的补充 Wonkaian 故事。我认为,其他回答者可能已经对熟悉的概念进行了最好的类比。就个人而言,我一直喜欢通过一个很好的类比来面对一个想法的真正起源,即radical novelty。我发现最广泛的类比是不恰当和令人困惑的。
你好,身份组合器λ x . x,你今天好吗?
我最喜欢这个答案。它刚刚解决了我所有的问题!
A
Ales Hakl

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


M
Michael Myers

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

J
Jon Davis

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


非常有趣。 :) 虽然年轻(呃)可能无法识别参考。
哈哈!是的,年轻的(我)还是能理解的……
我认为这是真的,我最终来到了这里。 youtube.com/watch?v=HyWqxkaQpPw 最近的文章futurism.com/scientists-made-real-life-flux-capacitor
我认为这个答案对于非英语人士来说可能特别令人困惑。在意识到它是一种幽默的流行文化参考之前(或从未意识到),人们可能会花费相当长的时间来理解这一说法。 (我喜欢它,如果我回答了这个问题并发现有人学习它会因此而气馁,我会感到难过)
M
Mark Bolusmjak

我已经在 Clojure 和 Scheme 中为 Y-Combinator 编写了一种“白痴指南”,以帮助自己掌握它。他们受到“小阴谋家”中材料的影响

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

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

这两篇教程都是代码中散布着注释,应该剪切和粘贴到你最喜欢的编辑器中。


您的分步方法非常好,谢谢分享。
D
Dapeng Li

作为组合器的新手,我发现 Mike Vanier's article(感谢 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-factorial 接受了一个 crappy 版本的阶乘函数(只能计算到数字 x - 1)并返回一个 less-crappy 版本的阶乘(计算直到数字 x)。如这种形式:

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

如果我们反复将 less-crappy 版本的阶乘传递给 almost-factorial,我们最终将获得所需的阶乘函数 f。它可以被认为是:

almost-factorial f = f

固定点

almost-factorial f = f 表示 f 的事实是函数 almost-factorialfix-point

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

三个功能

概括地说,我们有一个非递归函数fn(就像我们的几乎阶乘),我们有它的fix-point函数fr(就像我们的f) ,那么 Y 所做的就是当你给 Y fn 时,Y 返回 fn 的不动点函数。

所以总而言之(通过假设 fr 只接受一个参数来简化;x 在递归中退化为 x - 1x - 2...):

我们将核心计算定义为 fn: def fn fr x = ...accumulate x with result from (fr (- x 1)),这是几乎有用的函数 - 虽然我们不能直接在 x 上使用 fn,但它会是很快有用。这个非递归 fn 使用一个函数 fr 来计算它的结果

fn fr = fr,fr 是 fn 的不动点,fr 是有用的函数,我们可以在 x 上使用 fr 来得到我们的结果

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)

所以确实,(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

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

fn 和 fr 相互依赖:fr 在内部“包裹”fn,每次使用 fr 计算 x 时,它都会“生成”(“提升”?)一个 fn 并将计算委托给该 fn(传入 fr 和 x );另一方面,fn 依赖于 fr 并使用 fr 来计算较小问题 x-1 的结果。

在使用 fr 定义 fn 时(当 fn 在其操作中使用 fr 时),尚未定义真正的 fr。

是 fn 定义了真正的业务逻辑。在 fn 的基础上,Y 创建了 fr - 一个特定形式的辅助函数 - 以方便以递归方式计算 fn。

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

顺便说一句,我还发现这本书 An Introduction to Functional Programming Through Lambda Calculus 非常好,我只是参与其中,而且我无法理解书中 Y 的事实导致我写了这篇文章。


佚名

以下是从 answer by Nicholas Mancuso 中提到的 the article(完全值得一读)编译而来的 original questions 的答案以及其他答案:

什么是 Y 组合器?

Y-combinator 是一个“函数式”(或高阶函数——一个对其他函数进行操作的函数),它接受一个参数,这是一个非递归函数,并返回该函数的一个版本递归的。

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

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

另一种思考方式是,combinator 就是这样一个 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))

这意味着,对于给定的函数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 的主体评估。我问过 another question,与此主题有些相关。

它们有什么用?

Stolen 借自 answer by Chris Ammerman:Y-combinator 泛化递归,抽象其实现,从而将其与相关函数的实际工作分离。

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

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

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

正如 mentioned by Chris Ammerman:大多数过程语言都有静态类型。

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


T
Thomas Wagner

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


A
Andrew

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-combinator(y-combinator 将发散/无限循环)。


Y 组合器可以与按值传递和惰性求值一起使用。
T
Tires

this-operator 可以简化你的生活:

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)


z
zumalifeguard

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


你确定它不会产生比它解决的问题更多的问题吗?
Noctis,你能澄清你的问题吗?您是在问 y-combinator 的概念本身是否会产生比它解决的问题更多的问题,或者您是在谈论我选择特别使用 JavaScript 来演示的具体问题,还是我的具体实现或我建议通过自己发现它来学习它我描述的?