10种编程语言实现Y组合子

简介: Y组合子是Lambda演算的一部分,也是函数式编程的理论基础。它是一种方法/技巧,在没有赋值语句的前提下定义递归的匿名函数,即仅仅经过Lambda表达式这个最基本的“原子”实现循环/迭代。本文将用10种不一样的编程语言实现Y组合子,以及Y版的递归阶乘函数。
image.png编程

做者 | 技师
来源 | 阿里技术公众号数组

一 Y-Combinator
Y组合子是Lambda演算的一部分,也是函数式编程的理论基础。它是一种方法/技巧,在没有赋值语句的前提下定义递归的匿名函数。即仅仅经过Lambda表达式这个最基本的“原子”实现循环/迭代,很有道生1、一辈子2、二生3、三生万物的感受。闭包

1 从递归的阶乘函数开始app

先不考虑效率等其余因素,写一个最简单的递归阶乘函数。此处采用Scheme,你能够选择本身熟悉的编程语言跟着我一步一步实现Y-Combinator版的阶乘函数。编程语言

(define (factorial n)
(if (zero? n)函数式编程

1
(* n (factorial (- n 1)))))

Scheme中 (define (fn-name)) 是 (define fn-name (lambda)) 的简写,就像JS中,function foo() {} 等价于 var foo = function() {}。把上面的定义展开成Lambda的定义:函数

(define factorial
(lambda (n)工具

(if (zero? n)
  1
  (* n (factorial (- n 1))))))

2 绑定函数名
想要递归地调用一个函数,就必须给这个函数取一个名字。匿名函数想要实现递归,就得取一个临时的名字。所谓临时,指这个名字只在此函数体内有效,函数执行完成后,这个名字就伴随函数一块儿消失。为解决这个问题,第一篇文章中[1]强制规定匿名函数有一个隐藏的名字this指向本身,这致使this这个变量名被强行占用,并不优雅,所以第二篇文章[2]借鉴Clojure的方法,容许自定义一个名字。this

但在Lambda演算中,只有最普通的Lambda,没有赋值语句,如何绑定一个名字呢?答案是使用Lambda的参数列表!阿里云

(lambda (factorial)
(lambda (n)

(if (zero? n)
  1
  (* n (factorial (- n 1))))))

3 生成阶乘函数的函数
虽然经过参数列表,即便用闭包技术给匿名函数取了一个名字,但此函数并非咱们想要的阶乘函数,而是阶乘函数的元函数(meta-factorial),即生成阶乘函数的函数。所以须要执行这个元函数,得到想要的阶乘函数:

((lambda (factorial)
(lambda (n)

(if (zero? n)
   1
   (* n (factorial (- n 1))))))

xxx)
此时又出现另外一个问题:实参xxx,即形参factorial该取什么值?从定义来看,factorial就是函数自身,既然是“自身”,首先想到的就是复制一份如出一辙的代码:

((lambda (factorial)
(lambda (n)

(if (zero? n)
   1
   (* n (factorial (- n 1))))))

(lambda (factorial)
(lambda (n)

(if (zero? n)
   1
   (* n (factorial (- n 1)))))))

看起来已经把本身传递给了本身,但立刻发现 (factorial (- n 1)) 会失败,由于此时的 factorial 不是一个阶乘函数,而是一个包含阶乘函数的函数,即要获取包含在内部的函数,所以调用方式要改为 ((meta-factorial meta-factorial) (- n 1)) :

((lambda (meta-factorial)
(lambda (n)

(if (zero? n)
   1
   (* n ((meta-factorial meta-factorial) (- n 1))))))

(lambda (meta-factorial)
(lambda (n)

(if (zero? n)
   1
   (* n ((meta-factorial meta-factorial) (- n 1)))))))

把名字改为meta-factorial就能清晰地看出它是阶乘的元函数,而不是阶乘函数自己。

4 去除重复
以上代码已经实现了lambda的自我调用,但其中包含重复的代码,meta-factorial即作函数又作参数,即 (meta meta) :

((lambda (meta)
(meta meta))
(lambda (meta-factorial)
(lambda (n)

(if (zero? n)
   1
   (* n ((meta-factorial meta-factorial) (- n 1)))))))

5 提取阶乘函数
由于咱们想要的是阶乘函数,因此用factorial取代 (meta-factorial meta-factorial) ,方法一样是使用参数列表命名:

((lambda (meta)
(meta meta))
(lambda (meta-factorial)
((lambda (factorial)

(lambda (n)
    (if (zero? n)
      1
      (* n (factorial (- n 1))))))
(meta-factorial meta-factorial))))

这段代码还不能正常运行,由于Scheme以及其余主流的编程语言实现都采用“应用序”,即执行函数时先计算参数的值,所以 (meta-factorial meta-factorial) 原来是在求阶乘的过程当中才被执行,如今提取出来后执行的时间被提早,因而陷入无限循环。解决方法是把它包装在Lambda中(你学到了Lambda的另外一个用处:延迟执行)。

((lambda (meta)
(meta meta))
(lambda (meta-factorial)
((lambda (factorial)

(lambda (n)
    (if (zero? n)
      1
      (* n (factorial (- n 1))))))
(lambda args
  (apply (meta-factorial meta-factorial) args)))))

此时,代码中第4行到第8行正是最初定义的匿名递归阶乘函数,咱们终于获得了阶乘函数自己!

6 造成模式

若是把其中的阶乘函数做为一个总体提取出来,那就是获得一种“模式”,即能生成任意匿名递归函数的模式:

((lambda (fn)
((lambda (meta)

(meta meta))
(lambda (meta-fn)
  (fn
    (lambda args
      (apply (meta-fn meta-fn) args))))))

(lambda (factorial)
(lambda (n)

(if (zero? n)
   1
   (* n (factorial (- n 1)))))))

Lambda演算中称这个模式为Y组合子(Y-Combinator),即:

(define (y-combinator fn)
((lambda (meta)

(meta meta))

(lambda (meta-fn)

(fn
   (lambda args
     (apply (meta-fn meta-fn) args))))))

有了Y组合子,咱们就能定义任意的匿名递归函数。前文中定义的是递归求阶乘,再定义一个递归求斐波那契数:

(y-combinator
(lambda (fib)

(lambda (n)
  (if (< n 3)
    1
  (+ (fib (- n 1))
     (fib (- n 2)))))))

二 10种实现

下面用10种不一样的编程语言实现Y组合子,以及Y版的递归阶乘函数。实际开发中可能不会用上这样的技巧,但这些代码分别展现了这10种语言的诸多语法特性,能帮助你了解如何在这些语言中实现如下功能:

如何定义匿名函数;
如何就地调用一个匿名函数;
如何将函数做为参数传递给其余函数;
如何定义参数数目不定的函数;
如何把函数做为值返回;
如何将数组里的元素平坦开来传递给函数;
三元表达式的使用方法。
这10种编程语言,有Python、PHP、Perl、Ruby等你们耳熟能详的脚本语言,估计最让你们惊讶的应该是其中有Java!

1 Scheme
我始终以为Scheme版是这么多种实现中最优雅的!它没有“刻意”的简洁,读起来很天然。

(define (y-combinator f)
((lambda (u)

(u u))

(lambda (x)

(f (lambda args
      (apply (x x) args))))))

((y-combinator
(lambda (factorial)

(lambda (n)
  (if (zero? n)
      1
      (* n (factorial (- n 1)))))))

10) ; => 3628800
2 Clojure
其实Clojure不须要借助Y-Combinator就能实现匿名递归函数,它的lambda——fn——支持传递一个函数名,为这个临时函数命名。也许Clojure的fn不该该叫匿名函数,应该叫临时函数更贴切。

一样是Lisp,Clojure版本比Scheme版本更简短,却让我感受是一种刻意的简洁。我喜欢用fn取代lambda,但用稀奇古怪的符号来缩减代码量会让代码的可读性变差(我最近好像变得不太喜欢用符号,哈哈)。

(defn y-combinator [f]
(#(% %) (fn [x] (f #(apply (x x) %&)))))

((y-combinator
(fn [factorial]

#(if (zero? %) 1 (* % (factorial (dec %))))))

10)

3 Common Lisp

Common Lisp版和Scheme版其实差很少,只不过Common Lisp属于Lisp-2,即函数命名空间与变量命名空间不一样,所以调用匿名函数时须要额外的funcall。我我的不喜欢这个额外的调用,以为它是冗余信息,位置信息已经包含了角色信息,就像命令行的第一个参数永远是命令。

(defun y-combinator (f)
((lambda (u)

(funcall u u))

(lambda (x)

(funcall f (lambda (&rest args)
              (apply (funcall x x) args))))))

(funcall (y-combinator

(lambda (factorial)
        (lambda (n)
          (if (zerop n)
            1
            (* n (funcall factorial (1- n)))))))
     10)

4 Ruby

Ruby从Lisp那儿借鉴了许多,包括它的缺点。和Common Lisp同样,Ruby中执行一个匿名函数也须要额外的“.call”,或者使用中括号“[]”而不是和普通函数同样的小括号“()”,总之在Ruby中匿名函数与普通函数不同!还有繁杂的符号也影响我在Ruby中使用匿名函数的心情,所以我会把Ruby看做语法更灵活、更简洁的Java,而不会考虑写函数式风格的代码。

def y_combinator(&f)
lambda {|&u| u[&u]}.call do |&x|

f[&lambda {|*a| x[&x][*a]}]

end
end

y_combinator do |&factorial|
lambda {|n| n.zero? ? 1: n*factorial[n-1]}
end[10]

5 Python

Python中匿名函数的使用方式与普通函数同样,就这段代码而言,Python之于Ruby就像Scheme之于Common Lisp。但Python对Lambda的支持简直弱爆了,函数体只容许有一条语句!我决定个人工具箱中用Python取代C语言,虽然Python对匿名函数的支持只比C语言好一点点。

def y_combinator(f):

return (lambda u: u(u))(lambda x: f(lambda *args: x(x)(*args)))

y_combinator(lambda factorial: lambda n: 1 if n < 2 else n * factorial(n-1))(10)

6 Perl

我我的对Perl函数不能声明参数的抱怨更甚于繁杂的符号!

sub y_combinator {

my $f = shift;
sub { $_[0]->($_[0]); }->(sub {
    my $x = shift;
    $f->(sub { $x->($x)->(@_); });
});

}

print y_combinator(sub {

my $factorial = shift;
sub { $_[0] < 2? 1: $_[0] * $factorial->($_[0] - 1); };

})->(10);
假设Perl能像其余语言同样声明参数列表,代码会更简洁直观:

sub y_combinator($f) {
sub($u) { $u->($u); }->(sub($x) {

$f->(sub { $x->($x)->(@_); });

});
}

print y_combinator(sub($factorial) {
sub($n) { $n < 2? 1: $n * $factorial->($n - 1); };
})->(10);

7 JavaScript

JavaScript无疑是脚本语言中最流行的!但冗长的function、return等关键字老是刺痛个人神经:

var y_combinator = function(fn) {

return (function(u) {
    return u(u);
})(function(x) {
    return fn(function() {
        return x(x).apply(null, arguments);
    });
});

};

y_combinator(function(factorial) {

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

})(10);
ES6提供了 => 语法,能够更加简洁:

const y_combinator = fn => (u => u(u))(x => fn((...args) => x(x)(...args)));
y_combinator(factorial => n => n <= 1? 1: n * factorial(n - 1))(10);

8 Lua

Lua和JavaScript有相同的毛病,最让我意外的是它没有三元运算符!不过没有使用花括号让代码看起来清爽很多~

function y_combinator(f)

return (function(u)
    return u(u)
end)(function(x)
    return f(function(...)
        return x(x)(...)
    end)
end)

end

print(y_combinator(function(factorial)

return function(n)
    return n < 2 and 1 or n * factorial(n-1)
end

end)(10))
注意:Lua版本为5.2。5.1的语法不一样,需将 x(x)(...) 换成 x(x)(unpack(arg))。

9 PHP

PHP也是JavaScript的难兄难弟,function、return……

此外,PHP版本是脚本语言中符号($、_、()、{})用的最多的!是的,比Perl还多。

function y_combinator($f) {

return call_user_func(function($u) {
    return $u($u);
}, function($x) use ($f) {
    return $f(function() use ($x) {
        return call_user_func_array($x($x), func_get_args());
    });
});

}

echo call_user_func(y_combinator(function($factorial) {

return function($n) use ($factorial) {
    return ($n < 2)? 1: ($n * $factorial($n-1));
};

}), 10);

10 Java

最后,Java登场。我说的不是Java 8,即不是用Lambda表达式,而是匿名类!匿名函数的意义是把代码块做为参数传递,这正是匿名类所作得事情。

image.png

image.png
原文连接本文为阿里云原创内容,未经容许不得转载。

相关文章
相关标签/搜索