最近更新: 2010-06-23

透過 JavaScript,Ruby,PHP,C# 語言,理解 Java 的 Lambda 語法

日前 OpenJDK 發表了第一版的 Java Lambda 語法 (First Version of Java Lambda Syntax Sparks Debate),語法好壞,爭論不斷。本文分別列出了以 JavaScript, PHP, Ruby, C# 語言模仿 Java Lambda 範例的程式碼。

在數學中,lambda 有一個嚴謹的定義。但在是程式語言中,lambda 有另一個更廣泛的理解,即「匿名函數」(anonymous function)。在大多數程式語言中,根本沒有區分 lambda 與匿名函數,因此有些程式語言用 lambda 稱呼,有些程式語言用匿名函數稱呼。本文使用了五種程式語言,所以在稱呼上,會夾雜使用 lambda 與匿名函數。

Java
int i1 = #()(3).();
assertTrue(3 == i1);

Integer i2 = #()(3).();
assertTrue(3 == i2);

int i3 = #(int x)( x + 1 ).(3);
assertTrue(4 == i3);

Java 的 lambda 語法看來怪怪的?我熟悉的程式語言還算屈指可數,兩手並用就算得出來,我看到這語法是楞了一下,這還是 Java 嗎?專精 Java 的程序員看了更是不習慣,我同事第一眼還以為這是 jQuery 。

說它怪,但其實也不太難懂。待我點撥怎麼寫,立馬就會。就以 JavaScript 為例吧。先用 JavaScript 的語法寫出來,如下所示。

JavaScript
i1 = (function(){return 3})()
print(i1)

i2 = function(){return 3}()
print(i2)

i3 = function(x){return x + 1}(3);
print(i3)

JavaScript 不會主動將函式的最後一行結果視為回傳值,必須以 return 回傳。其他細項可參閱 The practice of anonymous recursion function in JavaScript

接著做三件事。第一,JavaScript 語法的 function 換成 Java lambda 語法的 #;第二,把包著函數本體的 { } 換成 ( );第三,Java 是靜態型別語言,所以你必須再為參數列中的每個參數加上型態宣告。這樣就完工了。閱讀方式請反向理解。

如果你想要在定義 lambda 之後立即調用,在 JavaScript 中,我們只需要直接在定義後套用「( )運算子」 operator ( )即可。但是 Java 並不將符號 ( ) 視為運算子,你想這麼做,就要在 lambda 本體與 ( )之間加上屬性連接符 .,因此就出現了 #()(3).() 這種句子 。只是這樣在語法上就出現分岐了,因為這種寫法看來仿彿 . 才是「( )運算子」。按 Java 一慣的語法,至少也該在連接符之後加上一個方法名稱,例如 #()(3).call(),讀起來也會清楚些。

在我看來,以上所述也涵蓋了 Java lambda 現行語法為人垢病之處。其實還不少,本文就列了四個。

  1. 新增符號 # 代表 lambda ,而不用關鍵字 functionlambda (從JavaScript 改寫為 Java lambda 的第一個動作)。在 Java 的語言文化中,向來是不喜用特殊符號做事的;反而在 shell script 很常使用特殊符號。所以這個用法,被人批為像是 Perl 的程式碼。
  2. 混用 ( ){ } 包覆 lambda 本體,而不是一貫採用表達程式碼區塊的 { } (從JavaScript 改寫為 Java lambda 的第二個動作)。看看其他語言的 lambda 語法,Java lambda 區分成用 ( ){ } 兩種使用情境,實在是找人麻煩啊。
  3. lambda 的參數列清單中,要加上型態宣告(從JavaScript 改寫為 Java lambda 的第三個動作)。若參數是 int 這種原生型態還好;若是自定義的類別,往往是長長一串名稱。如 lambda 要傳兩三個參數,說不定參數列清單的字數,就比 lambda 本體的程式碼字數還多。被這麼一搞,lambda 的使用價值可就打五折了。
  4. 直接調用 lambda 時,還要加一個連接符 .。具體缺點如上述,不再重複。

同樣的例子,我們再來看看其他程式語言如何寫。

PHP5.3 / PHP6

PHP5 到了 5.3版以後才增加匿名函數的語法,基本上照抄 JavaScript。細項請參閱 PHP 5.3/6 新增功能 - Closures 。此外,基於 PHP 的語法限制,它不允許你在定義後直接以 ()運算子 調用匿名函數。但是它可以直接作為其他函數的參數使用,所以我們可以用 call_user_func() 實現定義後立即調用的目的。

<?php
$f1 = function(){return 3;};
$i1 = $f1();
echo $i1, "\n";

$i2 = call_user_func( function(){return 3;} );
echo $i2, "\n";

$i3 = call_user_func( 
    function($x){return $x+1;}, 
    3 );
echo $i3, "\n";
?>
Ruby

Ruby 有相當靈活而強大的 closure 與 lambda 能力,與一致的表達語法。模仿本文的 Java 程式碼需要利用 Kernel.proc 實現。細項可參閱 Ruby block and Proc

i1 = lambda{3}.call
puts i1

# lambda equivalents to proc

i2 = proc{3}.call
puts i2

i3 = proc{|x| x + 1}.call(3)
puts i3
C# 3.0
using System;

public class Hello
{
    public static void Main()
    {
        Func<int> f1 = () => 3;
        var i1 = f1();
        Console.WriteLine(i1);
        
        //Sorry, I don't know how to do like this.
        //int i2 = (() => 3)();
        //Thanks to laneser.
        var i2 = new Func<int>( () => 3) ();
        Console.WriteLine(i2);

        Func<int, int> f3 = (x) => x+1;
        var i3 = f3(3);
        Console.WriteLine(i3);
        
        // LINQ use case:
        // var results = from(table).where(x => x > 100).select();
    }
}

單獨使用時, C# 的 lambda 語法看來也很奇怪。但若放入 LINQ 敘述中就不同了。渾然天成,仿彿一體。MSDN 有關於 C# Lambda 語法的詳細說明,請參閱 Lambda Expressions (C# Programming Guide) @ MSDN

樂多舊網址: http://blog.roodo.com/rocksaying/archives/12742243.html

樂多舊回應
laneser.kuo@gmail.com(laneser) (#comment-20888591)
Wed, 23 Jun 2010 16:56:20 +0800
我會建議 C# 的寫法如下, 比較接近您的範例的味道
Func f = () => 3;
int i1 = f();
Console.WriteLine(i1);

int i2 = new Func(() => 3)();
Console.WriteLine(i2);

int i3 = new Func(x => x + 1)(3);
Console.WriteLine(i3);
brianhsu.hsu@gmail.com(墳墓) (#comment-20889729)
Wed, 23 Jun 2010 20:42:41 +0800
附上 Scala 版的。:)

import java.lang.Integer
import java.lang.Number

val i1: Int = (() => 3)()
val i2: java.lang.Integer = (() => 3)()
val i3 = ((x: Int) => x + 1)(3)
val i4 = ((x: Number) => x.intValue)(3.14159)
val o = () => 3

assert (i1 == 3)
assert (i2 == 3)
assert (i3 == 4)
assert (i4 == 3)
assert (o != null)

幾種版本比起來,Java 的 lambda 真的怪怪的?
hoamon@gmail.com(hoamon) (#comment-20890051)
Wed, 23 Jun 2010 22:00:57 +0800
補上 Python 版:

i1 = (lambda: 3)( )
print(i1)

i2 = lambda: 3 ( ) #此語法結果不等於 i1 , 而是等於 lambda: 3
print(i2)

i3 = (lambda x: x+1)(3)
print(i3)
未留名 (#comment-20891543)
Thu, 24 Jun 2010 10:10:36 +0800
感謝 laneser, 墳墓, hoamon 的友情贊助,讓本文的內容更豐富了。

要說 Java lambda 語法的怪,是有比較依據的。以本文所列4條內容而言,第一條怪在它不合Java的語言文化。第二條怪在它一行時用(),多行用{},而不乾脆只用 {};第四條怪在中間那個「 . 」,看看其他語言,要嘛直接套用 ( ) ,要嘛在 . 後接個方法名稱,例如 ruby 的寫法。
未留名 (#comment-21370313)
Wed, 03 Nov 2010 12:36:35 +0800
sorry, 貼錯

Func f = () => 3;
int i1 = f();
Console.WriteLine(i1);

int i2 = new Func(() => 3)();
Console.WriteLine(i2);

int i3 = new Func(x => x + 1)(3);
Console.WriteLine(i3);
未留名 (#comment-21370311)
Wed, 03 Nov 2010 12:37:21 +0800
laneser的寫法無法執行, 得稍微修改成以下:

Func f = () => 3;
int i1 = f();
Console.WriteLine(i1);

int i2 = new Func(() => 3)();
Console.WriteLine(i2);

int i3 = new Func(x => x + 1)(3);
Console.WriteLine(i3);

另外, 個人實在不看好 Java 未來的發展, 近幾年新增加的語法(如本文提到的 Lamda function) 一整個把Java 語言的 style 搞亂. 感覺是在補破洞加一大堆亂七八糟的 token ('#'), 遠不如 C# 依然乾淨俐落.
未留名 (#comment-21370321)
Wed, 03 Nov 2010 12:40:33 +0800
抱歉, 好像 大於小於 內的字元都會被截掉, blog code的問題嗎? 所以可能 laneser原帖是正確的.
未留名 (#comment-21377579)
Fri, 05 Nov 2010 12:15:42 +0800
是的,樂多的回應系統會砍掉角括號字元,所以有些內容貼不上來。

就我後來補充的內容來看,laneser 的提示是正確的。

順便說一個好消息(?),Java7 確定不會納入 lambda 語法。不過 Java 語言仍然找不到新的活力。