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
原文連接本文爲阿里雲原創內容,未經容許不得轉載。

相關文章
相關標籤/搜索