关于函数式编程:什么是Y组合器?

关于函数式编程:什么是Y组合器?

What is a Y-combinator?

Y组合器从事物的功能方面讲是一种计算机科学概念。 大多数程序员对组合器一无所知,即使他们听说过它们也是如此。

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

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

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

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

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

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

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

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

1
2
3
4
5
6
// 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通过使用具有永久名称的函数来做到这一点,它可以引入递归。

1
2
3
4
5
6
7
8
9
10
11
// 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作为参数传递,仅在必要时在下一次迭代中调用。


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


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

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

我们的目标是能够编写1的递归函数
变量仅使用1个变量的函数而没有
作业,通过名称定义事物等(为什么这是我们的
目标是另一个问题,让我们将此作为
挑战我们。)似乎不可能,是吧?如
例如,让我们实现阶乘。

好吧,第一步就是说,如果我们
被骗了一点。使用2个变量的函数和
分配,我们至少可以避免使用
设置递归。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// 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内联。

1
2
3
4
5
6
7
8
9
10
11
12
// 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个变量
机械文本转换是这样的:

1
2
3
4
5
6
7
8
9
10
11
// Original
F = function (i, j) {
  ...
};
F(i,j);

// Transformed
F = function (i) { return function (j) {
  ...
}};
F(i)(j);

...保持完全相同。 (这个技巧叫做
发明者"狂奔"。 Haskell语言也是
以Haskell Curry的名字命名。将其归档为无用的琐事。)
现在只要将这种转换应用于任何地方,我们就可以
我们的最终版本。

1
2
3
4
5
6
7
8
9
10
11
12
// 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行替换为
您想要的任何其他递归函数。


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

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

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

1
2
3
4
5
6
7
function fact() {
    return function(n) {
        return n == 0 ? 1 : n * fact()(n - 1);
    };
}

var factorial = fact();

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

此阶段的递归仍然相当明确。 fact函数需要知道其自己的名称。让我们参数化递归调用:

1
2
3
4
5
6
7
8
9
10
11
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仍然需要知道它自己的名称。我们也将其参数化:

1
2
3
4
5
6
7
function recurser(f) {
    return fact(function(x) {
        return f(f)(x);
    });
}

var factorial = recurser(recurser);

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

1
2
3
4
5
6
7
function Y() {
    return (function(f) {
        return f(f);
    })(recurser);
}

var factorial = Y();

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

1
2
3
4
5
6
7
8
9
10
11
function Y() {
    return (function(f) {
        return f(f);
    })(function(f) {
        return fact(function(x) {
            return f(f)(x);
        });
    });
}

var factorial = Y();

唯一仍引用的外部名称是fact,但是现在应该很容易地对其进行参数化,以创建完整的通用解决方案:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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);
    };
});

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

定点组合器用于显示lambda演算已完成。这在计算理论中是非常重要的结果,并为函数式编程提供了理论基础。

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


JavaScript中的y-combinator:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
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如何做魔术的完整解释,请查看链接文章(不是我本人)。


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

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

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

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

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

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

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

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

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


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


匿名递归

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

1
forall f.  fix f  =  f (fix f)

fix f表示定点方程的解x

1
               x  =  f x

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

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

使用fix,可以在没有非一致的自引用的情况下得出关于一般/μ递归函数的任意构造证明。

1
fact n = (fix fact') n

哪里

1
2
3
fact' rec n = if n == 0
                then 1
                else n * rec (n - 1)

这样

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
   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

这种形式上的证明

1
fact 3  =  6

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

1
fix fact'  ->  fact' (fix fact')

λ演算

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

1
2
3
E ::= v        Variable
   |  λ v. E   Abstraction
   | E E      Application

其中v覆盖变量,以及beta和eta减少规则

1
2
(λ 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消除了多余的抽象。有时从形式主义中将其省略。不适用归约规则的不可归约表达式为正则或规范形式。

1
λ x y. E

是的简写

1
λ x. λ y. E

(抽象多元性),

1
E F G

是的简写

1
(E F) G

(应用程序左关联),

1
λ x. x

1
λ y. y

是与alpha等效的。

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

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

1
2
3
4
5
6
7
8
9
10
   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
1 + 2  =  3

使用beta减少的重写规则:

1
2
3
4
5
6
7
8
9
   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,身份组合器

1
λ x. x

身份函数同构

1
id x = x

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

1
2
3
S  =  λ x y z. x z (y z)
K  =  λ x y. x
I  =  λ x. x

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

1
λ x. x x

对自己:

1
2
3
4
   (λ x. x x) (λ y. y y)
=  (λ y. y y) (λ y. y y)
. . .
=  _|_                     Bottom

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

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

在急切的应用顺序Beta减少下产生分歧

1
2
3
4
5
=  (λ 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))
. . .
=  _|_

因为严格的语义

1
forall f.  f _|_  =  _|_

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

1
2
3
=  (λ l. ((λ i. i) a)) ((λ x. x x) (λ y. y y))
=  (λ l. a) ((λ x. x x) (λ y. y y))
=  a

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

?

Y定点组合器的基本属性

1
λ f. (λ x. f (x x)) (λ x. f (x x))

是(谁)给的

1
2
3
4
5
6
   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))
. . .                                      . . .

等价

1
Y g  =  g (Y g)

同构

1
fix f  =  f (fix f)

未类型化的lambda演算可以对通用/μ递归函数上的任意构造性证明进行编码。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
 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外,还存在定点组合器的递归可枚举无穷大。

1
2
3
4
5
 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中,定点组合器可以轻松实现

1
2
fix :: forall t. (t -> t) -> t
fix f = f (fix f)

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

1
2
3
4
5
6
primes :: Integral t => [t]
primes = sieve [2 ..]
   where
      sieve = fix (\ rec (p : ns) ->
                     p : rec [n | n <- ns
                                , n `rem` p /= 0])
  • 大卫·特纳(David Turner):丘奇的论文和函数式编程
  • 阿隆佐·丘奇:基本数论的一个无法解决的问题
  • λ演算
  • 丘奇-罗瑟定理

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


这是Y-Combinator和Factorial函数的JavaScript实现(摘自Douglas Crockford的文章,网址为:http://javascript.crockford.com/little.html)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
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);


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

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

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

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


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

What is a Y-combinator?

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

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

组合器-只是没有自由变量的lambda表达式。

自由变量—是不是绑定变量的变量。

绑定变量-包含在以该变量名作为其自变量之一的lambda表达式内的变量。

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

Y组合器是定点组合器。

函数的不动点是函数域的元素,该域由函数映射到自身。

也就是说,如果f(c) = cc是函数f(x)的固定点

这意味着f(f(...f(c)...)) = fn(c) = c

How do combinators work?

下面的示例假定使用强类型+动态类型:

惰性(常规)Y组合器:

此定义适用于具有延迟(也称为:延迟,按需调用)评估的语言-评估策略,该评估策略将对表达式的评估延迟到需要其值为止。

1
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组合器:

此定义适用于具有严格(也包括:急切,贪婪)评估的语言-一种评估策略,在该策略中,将表达式绑定到变量后立即对其进行评估。

1
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的身体评估。我问了另一个问题,与这个话题有些相关。

What are they good for?

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

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

Are they useful in procedural languages?

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

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

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


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

好。

从胡扯到少胡扯

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

好。

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

在上面的伪代码中,almost-factorial接受函数f和数字x(咖喱almost-factorial,因此可以将其视为接受函数f并返回1-arity函数)。

好。

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

好。

可以看到,almost-factorial接受了cr脚版本的阶乘函数(只能计算直到编号x - 1),并返回了-脚程度较低的阶乘函数(计算到数量x)。以这种形式:

好。

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

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

好。

1
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)),这是几乎有用的功能-尽管我们不能直接在x上使用fn,但这很快就会有用。此非递归fn使用函数fr计算其结果
  • fn fr = frfrfn的固定点,fr是有用的函数,我们可以在x上使用fr来获得结果
  • Y fn = frY返回函数的固定点,Y将几乎有用的函数fn转换为有用的fr
  • 好。

    导出Y(不包括)

    我将跳过Y的派生,然后去理解Y。 Mike Vainer的帖子有很多细节。

    好。

    Y的形式

    Y定义为(以lambda演算格式):

    好。

    1
    Y f = λs.(f (s s)) λs.(f (s s))

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

    好。

    1
    2
    3
    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)仅采用一个参数,例如阶乘函数。

    好。

    1
    def fn fr x = accumulate x (fr (- x 1))

    fn fr = fr开始,我们继续

    好。

    1
    2
    3
    => 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

    好。

    1
    2
    fr = Y fn = λs.(fn (s s)) λs.(fn (s s))
    => fn (λs.(fn (s s)) λs.(fn (s s)))

    所以

    好。

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

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

    好。

  • fnfr相互依赖:fr'包装'fn在内部,每次使用fr计算x时,它都会生成fn("提升"?)。并将计算委托给fn(本身传递frx);另一方面,fn依赖于fr,并使用fr来计算较小问题x-1的结果。
  • 在使用fr定义fn时(当fn在其操作中使用fr时),实际的fr尚未定义。
  • fn定义了真正的业务逻辑。 Y基于fn创建fr(一种特定形式的辅助函数),以递归的方式简化对fn的计算。
  • 好。

    它现在帮助我以这种方式理解Y,希望对您有所帮助。

    好。

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

    好。

    好。


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

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

    你可以做

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

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


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

    1
    2
    3
    4
    5
    6
    7
    8
    9
    var Y = function(f) {
        return (function(g) {
            return g(g);
        })(function(h) {
            return function() {
                return f.apply(h(h), arguments);
            };
        });
    };

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

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

    最后,您调用fac(5)


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


    我认为回答这个问题的最好方法是选择一种语言,例如JavaScript:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    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解决的问题。


    推荐阅读