01 January 2007

Java 7 的 Closure 在 2006 年末時引起廣泛的討論,對於到底 Java 該不該加入 closure 爭議不休。 本文是個人的一些小心得,內容是參照並引用目前最受囑目的 BGGA Java Closure 草案來撰搞。 BGGA 意指 Gilad Bracha, Neal Gafter, James Gosling, Peter von der Ahé 等四位大師的頭文字組合,而最主要的推手是大師 Neal Gafter (Java Puzzler 的作者之一)。

什麼是 Closure ?

Closure 在英文裡是終止、或是打烊的意思。講白一點,就是描述一個從開到關的動作 這兩個狀態跟程式有什麼關係呢?我們先來看兩個數學的函數:

(1)   f(x) = x + 1
(2)   f(x) = x + y + 1

式 (1) 是一個標準的 x 函數,代入 x 便可得一值。式 (2) 也是一個 x 的函數,但裡面多了一個不相干的變數 y, 在這裡我們稱變數 y 為自由變數。在程式界,我們會說式 (1) 因為沒有自由變數,所以是關閉的, 而相對的,式 (2) 含有自由變數 y,則稱為開啟的。

好了,現在我們將式 (2) 的自由變數 y 代入另一個值,假設是 3 好了,那麼式 (2),就會變成 f(x) = x + 4。注意到沒?現在式 (2) 已經關閉了。我們將式 (2) 從開的狀態變成關閉的狀態 - "代入自由變數" (Binding) 這個動作就是我們剛才提到的 Closure 啊!

Closure 原意 - 將自由變數代入 (binding) 其他值

Binding 自由變數,這就是 Closure 最原始的意思。然而,程式界漸漸的已經不在使用這個原來的意思了。 現在的意思是什麼?我們來看一段虛構的 java 7 程式碼:

//A pseudo code
public void test() {
    int y = 1;
    
    ex.execute( { //anonymous function 
        x => x + y + 1; 
    } );
}

上面的 test() method 裡有一 local 變數 y,而 ex.execute() 裡面寫了一個不具名的函數 - { x => x + y + 1; } 其意義等同於f(x) = x + y + 1, 而它裡面用到了一外部的自由變數 y。這時,我們稱這個可使用自由變數 y 的不具名函數為 closure。 按此邏輯推衍,更明確的定義是:

Closure 現行定義 - 可直接處理 block 外資源的不具名函數

"block 外資源" 即 不具名函數的 { } 外面的 return, break, 以及存取自由變數等等。目前的定義顯然和原來的意思相差甚遠, 而且常常讓許多人搞混,因此很多具備 "Closure" 功能的語言,多半不使用 Closure 這個詞, 多採用其他較正確的說法像是 anonymous function 或是 lambda expression 等等。目前的 java 7 closure 草案 則是採用現行定義的說法。(也許未來會改正?)

Closure 有什麼用?

"可直接處理 block 外資源的不具名函數",在撰寫程式非常有用的,尤其對很小的邏輯而言。如果 java 有 closure, 很多重覆的程式碼都可以去除,例如將一個 String 的 List parse 為 Integer:

//原本 Java 的寫法
public List<Integer> convertStringToInteger(List<String> strings) {
    List<Integer> ints = new ArrayList<Integer>();
    for(String s : strings) {
       ints.add(Integer.parseInt(s));
    }
    return ints;
}

//改成 Closure:
public List<Integer> convertStringToInteger(List<String> strings) {
    return Collections.convert(strings, {String s => Integer.parseInt(s) });
}

或者是常見的讀取 InputStream:

//原本 Java 的寫法
public void readData(InputStream inputStream) throws IOException{
    try {
        readFromInput(inputStream); //這個 method throws IOException
    } finally {
        try {
            if(inputStream !=null) {
                inputStream.close();
            }
        } catch (IOException ignore) {}
    }
}

//改成 Closure:
public void readData(InputStream inputStream) throws IOException {
    IO.with(inputStream, { InputStream in => 
       readFromInput(in) ; //這個 method throws IOException
    });
}

實做的細節我們先不管,光看結果就知道使用 closure 讓程式變簡短很多,像是第一個例子去除了多餘的 local 變數 List<Integer> ints。第二個例子更是將初學者最常出錯的 try catch finally close 都藏起來了。 簡明、減少出錯、程式易讀,這三個是最直接的好處。

現行 Java 可以用 Anonymous Inner Class 達到類似的效果

是的,現行的 Anonymous Inner Class (AIC) 可以做到上面類似 closure 的效果, 這也是反對 Closure 加入 Java 這一方陣營的說詞。我們來看看 AIC 怎麼模擬 closure 的功能, 在此之前,我們要先準備一些工具:

//先有個 interface,代入 T 回傳 R
public interface Converter<R, T> {
    public R invoke(T arg) ;
}

//然後是 Collections 上 convert 的工具 method:
public static <T,R> List<R> convert(List<T> originals, Converter<R, T> converter) {
	List<R> converteds = new ArrayList<R>();
	for( T t : originals ) {
		converteds.add(converter.invoke(t));
	}
	return converteds;
}

上述的工具即使是 Closure 也會用到,差別在於用法及適用範圍不一樣。比較 AIC 和 Closure 兩者的寫法:

//AIC 寫法
public List<Integer> convertStringToInteger(List<String> strings) {
    return Collections.convert(strings, new Converter<Integer, String>() {
    	public Integer invoke(String orignal) {
    	    return Integer.parseInt(orginal) ;
    	}
    });
}

//Closure 寫法 (跟前面的範例一樣,這裡重覆方便做個比較)
public List<Integer> convertStringToInteger(List<String> strings) {
    return Collections.convert(strings, {
        String s => Integer.parseInt(s) 
    });
}

耶? AIC 字雖然多了點,但其實也不賴啊,它也不用多餘的 local 變數 List<Integer> ints ,而且現在 IDE 這麼強,打的字跟 Closure 的版本其實差不多呢!但如果改變寫法, 例如你想使用外部的 Integer parser 時,AIC 就出錯了:

//位於同 class 的 parser method
public Integer customParseInt(String s) {
    // 客製的轉換,例如將國字 "一" 轉為 1
}

//AIC 寫法
public List<Integer> convertStringToInteger(List<String> strings) {
    return Collections.convert(strings, new Converter<Integer, String>() {
    	public Integer invoke(String orignal) {
    	    // compile error !!
    	    return customParseInt(orginal) ; 
    	}
    });
}

//Closure 寫法
public List<Integer> convertStringToInteger(List<String> strings) {
    return Collections.convert(strings, {
        String s => customParseInt(orginal) //沒問題!
    });
}

Closure 可以使用 block 以外的資源,所以可以直接呼叫 customParseInt(),但 AIC 就不行了, compiler 會抱怨 Converter 沒有 customParseInt() 這個 method 可用。你得改成像是 FooClass.this.customParseInt() 這種怪寫法才行。

好,我們再來看看第二個 InputStream 的例子,同樣的先準備一些工具:

//先有個 interface,可代入任何 Closable 的 class (InputStream/OutputStream...等等)
public interface Block<T extends Closable> {
    public void invoke(T closable) ;
}

//然後是 IO class 上 with() 的工具 method:
public static <T extends Closable> void with(T resource, Block<T> block) {
    try {
        block.invoke(resource);
    } finally {
        try {
            if(resource!=null) {
                resource.close();
            }
        } catch (IOException ignore) {}
    }
}

好,比較一下吧:

//AIC 寫法
public void readData(InputStream inputStream) throws IOException {
    IO.with(inputStream, new Block<InputStream>() {
        public void invoke(InputStream in) {
            //compile error! 無法 throw IOException 到 readDate()!
            readFromInput(in);
        }
    });
}

//Closure 寫法 (跟前面的範例一樣,這裡重覆方便做個比較)
public void readData(InputStream inputStream) throws IOException {
    IO.with(inputStream, { InputStream in => 
        readFromInput(in) ; //可正確 throw IOException 到 readData()
    });
}

AIC 的版本根本無法處理 readFromInput(in) 會丟出來的 checked exception。 但 closure 就沒問題了。你也許會問,將 Block的 invoke 和IO 的 with() 都加上 throws IOException 不就得了嗎?但如果我有其他 exception 呢?

//AIC 寫法,這一次 with() 和 invoke() 都可接 IOException
public void readData(InputStream inputStream) throws IOException {
    IO.with(inputStream, new Block<InputStream>() {
        public void invoke(InputStream in) throws IOException {
            readFromInput(in); //IOException ok
            
            //compile error again !!
            anotherLogicThrow_NumberFormatException() ;
        }
    });
}

//Closure 寫法
public void readData(InputStream inputStream) 
                 throws IOException, NumberFormatException {
    IO.with(inputStream, { InputStream in => 
        readFromInput(in) ;                         //ok!
        anotherLogicThrow_NumberFormatException() ; //ok!
    });
}

處理完了 IOException,又來個 NumberFormatException,總不能這樣一直加下去吧? 相對的 closure 就沒這種困擾了,處理起來直覺又簡單。除了上述的例子之外, AIC 也不能做 break/return、存取自由變數等等動作,限制真的太多了, 所以 - AIC 無法取代 closure 的所有用途,Java 是需要 closure 的。

Java Closure 的文法

直接看範例吧:

//代入一 x,算出 x + 1 並傳回。類似 f(x) = x + 1
{ int x => x + 1 }

//代入 x, y,算出 x + y 的和並傳回。類似 f(x,y) = x + y
{ int x, int y => x + y }

//代入 一 String,轉成 int 傳回
{ String s => Integer.parseInt(s) }

//執行任意的statement,沒有傳回值
{ => System.out.println("hello"); }

    規則如下:

  • {} 大括號為界定範圍
  • 使用 => 箭頭分隔輸入和輸出
  • 左邊是宣告參數的型別和變數名,逗點隔開,沒有則留白
  • 右邊是 statement block,可以多個,最後一行可寫 expression,然後直接回傳值

不懂?好,來看看 AIC 怎麼轉 closure 就比較容易理解了:

//類比的 AIC:
new Block() {
    public int sum(int x, int y) {
        System.out.println("some msg...");
        return x + y ; 
    }
};

//對應的 closure 寫法
{ int x, int y => //參數列
    System.out.println("some msg..."); //中間可以多個 statement
    x + y //最後一行可以是 expression,不用寫 return
}

1:1 的比較應該好懂吧,就把位置換一換就好了。事實上依目前的 closure 草案,closure 在背後就是轉為 interface 來做的 (closure conversion),所以對 JVM 來說, AIC/Closure 兩者是沒有差別的 (這也是重要的考量之一,更動 VM spec 的代價太高了!)。真正做事的是 compiler ,它會替我們做語法和 block 外部環境的處理等等鎖事。前面的例子已經介紹了 closure 可以處理外部的 method 和 exception, 下面是 return 和 存取自由變數的例子:

//存取自由變數:
public void compute() {
   int x, y = .... ;
   int result = 0 ;
   summation( { int x, int y =>
      result = x + y ; //可直接 assign 給自由變數
   } );
}

//直接中斷 return:
public String findMatch(List<String> strings) {
   Collections.each(strings, { String s =>
       if(matched(s)) {
           return s ; //可從 closure 內部直接離開 findMatch() method
       }
   });
   return null; //not found
}

當然,closure conversion 還是有些限制,就是計算回傳值的 expression 一定要放最後一行, 像上面的 x+y 只能放在最後一行,不能提前結束。為什麼不能提前?因為 return 在 closure 內, 是用來跳出外部 method 的,不是用來回傳值的。

自訂控制語法

還沒完!Java 新加入的 Closure 如果只有這樣就太遜了,Java Closure 還有一個重要的任務 - 替 Java 增加自訂控制語法的能力 (Enable Control Abstraction)。Java 語言已內建 for/if/while/switch 等等控制語法了,新的 Closure 將提供 developer 自訂語法的能力。

//假設原本的 closure 是這樣用:
public void doSomething() {
    foo("ok", { =>
        System.out.println("hello");
    });
}

//我們可以改寫成:
public void doSomething() {
    foo("ok") { 
        System.out.println("hello");
    }
}

注意到了沒?幾個符號的調換和省去之後,變成 foo(String) { }了,這個寫法像 if 吧? 我們創造了新的 foo 這個控制語法。再來看看更實際的範例:

//原本的 closure 是這樣用:
IO.with(makeInputStream(), { InputStream in => 
    readFromInput(in) ; 
});

//我們可以改寫成:
IO.with( InputStream in : makeInputStream() ) {
    readFromInput(in);
}

這是剛才的 IO.with 範例,改寫後變成 with(InputStream in : makeInputStream() ) { } 了, 這個寫法像 jdk5 的 enhanced for 吧?新的控制語法 with 誕生!至於語法怎麼寫自然有一套規則, 這裡就先略過了。反正只是搬搬位置,加個冒號就成了。

但要能做到自訂控制語法可不是 compiler 語法搬搬家而已,最重要的還是 closure 的"處理外部資源" 的能力, 可以存取自由變數和直接 return 都是關鍵所在。

自訂控制語法將賦與 Java 新的生命,成為一個可快速進化的語言

兼容原有的 API

前面提到 closure conversion 其實背後是用 interface 來實作的,這意味這原有的 interface 也能直接改寫成 closure。前提是該 interface 只能有一個 method,下面是幾個範例:

//範例一:使用 Executor 執行一 task:
void launch(Executor ex) {
    ex.execute(new Runnable() {
        public void run() {
            doSomething();
        }
    });
}

//改寫成 closure:
void launch(Executor ex) {
    ex.execute({ =>
        doSomething();
    });
}

//再簡化為控制語法
void launch(Executor ex) {
    ex.execute(){ 
        doSomething();
    }
}
//範例二:Swing listener
void addListener(ItemSelectable is) {
    is.addItemListener(new ItemListener() {
        public void itemStateChanged(ItemEvent e) {
            doSomething(e, is);
        }
    });
}

//改寫成 closure:
void addListener(ItemSelectable is) {
    is.addItemListener( { ItemEvent e =>
        doSomething(e, is);
    });
}

//再簡化為控制語法
void addListener(ItemSelectable is) {
    is.addItemListener( ItemEvent e :) { //轉換後居然變成笑臉符號了!
        doSomething(e, is);
    }
}

範例二的控制語法後來變成一個 :) 笑臉符號了 (因為原來的 method 沒有引數),Neal Gafter 是說雖然可以寫成這樣,但還是盡量避免,以免造成不必要的困擾。

Java Closure 帶來的新可能性

看看這個為 Map 設計的新的 forEach

Map<Key, Value> map = ...;
eachEntry(Key k, Value v :map) {
     //operate with k, v here
}

你是否跟我一樣也哈這種寫法很久了?有了 closure 你現在可以自己定義!

再來是 IO.with() 的衍生應用,這是一個 input 複製到 output 的 copy 功能:

with(InputStream in : makeIn()) with(OutputStream out : makeOut()) {
    // 複製 "in" 的內容到 "out"
}

看傻眼了嗎?後半段的 with(out) {}其實是前面的 with(in) 的 closure (因為只有一行所以省略大括弧)。主程式段的 { } 則是後半段 with(out) 的 closure。也許一開始不熟, 久了之後一定沒問題的啦,換來的是可讀性更高、更不易出錯的程式。

那有沒有可能這樣呢?

select("*") from ("my_table") where() {
 	//some conditions   
}

還可以直接寫 SQL 的語法呢!而 AOP 呢? 其他 framework 呢? 自訂控制語法將帶給 Java 新的無限可能性啊!

也許有人會擔心自訂語法將替 Java 帶來分歧 (例如 Joshua Block 大師就是...),要學別人的程式的自創語法。 Neal Gafter 則認為這個學習難度和學新的 API 沒差多少 - 看別人的 code 本來就要 他用的 API, 今天的 xml library 有 12 種,你換了一個 library 大概全部要重學。大家怎麼學別人的 API?還不是在 IDE 點一下, 看 javadoc or source 嗎?同理,自創的語法也一樣,點進去看就好了,學習曲線差不多。

最後,Java Closure 草案是 Neal Gafter 大師利用自己的公餘時間草擬和推廣的(他已經不是 sun 的員工了),真是無私奉獻啊! 小弟就用這一篇跨年以表達小小的敬意!

參考資料


回響

可以用 Tag <I>、<B>,程式碼請用 <PRE>