各語言Y組合子大比拼
最近有點無聊,突然想試試在各種語言裡面實現Y組合子。不過寫完之後,沒想到結果完全出乎我的意料。嘛,讓我們來看看不同語言裡的Y組合子。
首先祭上Y組合子的定義:
Y = \lambda f. (\lambda x. f(x \, x)) \, (\lambda x. f(x \, x))
Python魔法
和眾多流行的弱型別語言一樣,Python支援lambda表示式但不支援延遲求解和柯里化,所以Python的寫法應該也是比較有代表性的。
Y = lambda f: (lambda x: f(x(x)))((lambda x: f(lambda *args: x(x)(*args))))
可以看到,雖然有lambda加持,然而由於lambda這個單詞真的好長,所以整個式子就變成了這樣。另外,由於遞迴函式實際引數是傳至右式的,所以左式並不需要傳args。
JavaScript魔法
大部分如lua、php和Python並沒有太大的區別。不過,時至ES6,js已經有了自己的lambda表示式——箭頭函式。
Y = f => (x => f(x(x))) ((x => f(...a => x(x)(...a))))
由於無法脫離“函式呼叫要加括號”的苦海,於是縱使有簡單的lambda寫法,JS裡成山的括號依舊令人無法直視。
CoffeeScript黑魔法
熟悉我的人一定知道,我個人是cs的腦殘粉。cs的簡潔與靈活和js(尤其是es5)真是天壤之別,函式呼叫可以省略括號也提供了極大的便利。
Y = (f) -> ((g) -> f (g g)) ((g) -> f (...x) -> (g g) ...x)
除了不支援柯里化導致不可避免的需要x,其餘可以說是很完美了。
Haskell
那支援柯里化的語言是不是就無敵了?少年,天真了。
import Unsafe.Coerce y :: (a -> a) -> a y = \f -> (\x -> f (unsafeCoerce x x)) (\x -> f (unsafeCoerce x x))
由於Haskell有嚴格的型別檢查,於是Y組合子這種“無限遞迴型別”的函式是無法通過型別檢查的。不過,難道就沒有不通過unsafeCoerce的方法嘛?想了一晚上,腦闊疼。不過作為SOO(StackOverflow Oriented)程式員我還是成功的找到了答案(原答案地址:ofollow,noindex" target="_blank">https://stackoverflow.com/a/5885270 )。
newtype Mu a = Mu (Mu a -> a) y f = (\h -> h $ Mu h) (\x -> f . (\(Mu g) -> g) x $ x)
讓我們來分析下這段程式碼。首先先用β歸約:
y f = (\x -> f . (\(Mu g) -> g) x $ x) $ Mu (\x -> f . (\(Mu g) -> g) x $ x)
再次β歸約:
y f = f . (\(Mu g) -> g) Mu (\x -> f . (\(Mu g) -> g) x $ x) $ Mu (\x -> f . (\(Mu g) -> g) x $ x)) = f . (\x -> f . (\(Mu g) -> g) x $ x) $ Mu (\x -> f . (\(Mu g) -> g) x $ x)) = f . y f
可以看出,這和Y組合子是等價的。而引入的Mu也同樣起到了防止遞迴型別檢查的效果。
Lisp(Scheme)
Haskell那邊栽跟頭主要是因為型別檢查,那Lisp呢?(出處 )
(define (Y f) (lambda (f) ((lambda (x) (f (lambda (arg) ((x x) arg)))) (lambda (x) (f (lambda (arg) ((x x) arg)))))))
Scheme還是很純粹很好看的。
Java作為一個有型別檢查,lambda是語法糖的語言,想寫Y組合子必然是痛苦的。我從Gists 上找了一個:
package test; import java.math.BigInteger; import java.util.function.Function; public class YCombinatorFactorial<T> { private interface Improver<T> { Function<T,T> apply( Improver<T> f ) ; } private Function<T,T> Y( final Function<Function<T,T>,Function<T,T>> r ) { return ((Improver<T>)f -> f.apply( f )).apply( f -> r.apply( x -> f.apply( f ).apply( x ) ) ) ; } public static void main( String[] args ) { YCombinatorFactorial<BigInteger> yf = new YCombinatorFactorial<BigInteger>() ; BigInteger result = yf.Y( f -> n -> n.equals( BigInteger.ZERO ) ? BigInteger.ONE : n.multiply( f.apply(n.subtract( BigInteger.ONE ) ) ) ).apply( BigInteger.valueOf( 100 ) ) ; System.out.println( result ); } }
不過老實說,引入了lambda之後確實簡潔了不少。這裡簡單解釋一下。首先還是做β變換:
((Improver<T>)f -> f.apply( f )).apply( f -> r.apply( x -> f.apply( f ).apply( x ) ) ); (f -> r.apply( x -> f.apply( f ).apply( x ) )).apply( f -> r.apply( x -> f.apply( f ).apply( x ) ) );
其實也不用過多解釋了,這樣的形式已經和Y組合子的定義一致了。
WolframScript真魔法
由於純函式支援類scala的佔位符_,所以ws寫出來真是又簡潔又看不懂。(程式碼來自網際網路)
yCombinator@f_ := #@# &[Function[n, f[#@#]@n] &];
果然還是喜歡CoffeeScript啊……