簡介: Y組合子是Lambda演算的一部分,也是函數式編程的理論基礎。它是一種方法/技巧,在沒有賦值語句的前提下定義遞歸的匿名函數,即僅僅經過Lambda表達式這個最基本的「原子」實現循環/迭代。本文將用10種不一樣的編程語言實現Y組合子,以及Y版的遞歸階乘函數。編程
做者 | 技師
來源 | 阿里技術公衆號數組
一 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表達式,而是匿名類!匿名函數的意義是把代碼塊做爲參數傳遞,這正是匿名類所作得事情。
原文連接本文爲阿里雲原創內容,未經容許不得轉載。