iThome 網站首載:物件導向語言中的一級函式
現代物件導向語言中常見一級函式的存在,其概念主要來自lambda演算(lambda calculus),物件導向語言中以某種形式的lambda語法,為原本語言提供了一個小型通用語言,以便在面對某些問題領域,提供以lambda演算解題的可能性。
- 函式作為一級值
程式中可被指定給變數的東西稱為值,如果值可以傳給函式的參數或從函式中傳回,稱為一級值(First-class value)。有些物件導向程式語言中,不僅基本型態及物件,函式亦具有一等公民(First-class citizen)的地位,它們可以匿名地定義、指定給變數、傳入函式或從函式中傳回,現代開發者最熟悉具一級函式的語言就是JavaScript,可使用function關鍵字來定義匿名函式。
函式作為一級值主要受到lambda演算的影響,想要瞭解或善用一級函式,就得瞭解基本的lambda演算概念。在lambda演算中,每個表達式(Expression)代表具單一參數的函數,參數本身亦可接受具有單一參數的函式。例如,函數f(x) = x * 2可匿名地表達為x -> x * 2(或表達為λ x. x * 2,以下採前者表示方式),如果要套用x為2,可表示為(x -> x * 2)(2)。如果有函數g(y) = y - 1,想表達g(f(x)),可以匿名地寫為(y -> y - 1)(x -> x * 2),套用後成為x -> x * 2 - 1,函數傳入另一函數相當於組合出新函數。
多參數的函數可使用單獨參數的函數套用而成,例如(x, y) -> x * x + y * y可表示為x -> (y -> x * x + y * y),如果x為2而y為3,則套用過程為(x -> (y -> x * x + y * y)(2))(3) = (y -> 2 * 2 + y * y)(3) = 4 + 3 * 3 = 4 + 9 = 13,原先的x -> (y -> x * x + y * y稱為鞣製函式(Curried function),函數套用x後傳回新函數y -> 1 + y * y,稱為部份套用函式(Partially applied function),之後再以新函數套用y的值。類似地,透過一套規則定義,lambda表達式可用來表現任何可計算函數,連if等控制結構也可使用函數來表示。
不同程式語言會提供不同程度的lambda表達式,例如(x -> x * 2)(2)以JavaScript來表達則為:
function(x) {
return x * 2;
}(2);
return x * 2;
}(2);
JavaScript函式可接受函式作為參數,亦可將函式作為傳回值,不過不支援函式部份套用(Partially application),必須自行實作才能達到鞣製函式效果。Java至JDK7為止都沒有支援一級函式,JDK8將導入lambda語法及相關支援,探討Java何以要導入lambda語法,有助於瞭解一級函式在物件導向語言中的角色...
- Java匿名內部類別與lambda語法
Java一直存在是否導入lambda語法的爭議,反對者所持理由之一是,Java中存在著lambda語法的類似品,也就是匿名內部類別(Anonymous inner class)。若要使用匿名內部類別模擬一級函式,可定義單一抽象方法(Single abstract method)介面,例如:
interface Func<P, R> {
R apply(P p);
}
R apply(P p);
}
其中P代表參數,R代表傳回值,也就以apply方法簽署的參數與傳回值,來代表一級函式的參數與傳回值宣告。如果要使用匿名內部類別來表示x -> x * 2,則必須寫為:
new Func<Integer, Integer>() {
public Integer apply(Integer x) {
return x * 2;
}
}
public Integer apply(Integer x) {
return x * 2;
}
}
實際上,開發者只關心x -> x * 2,也就是函數的參數與執行內容,匿名內部類別語法顯而易見地,迫使開發者得額外留意介面名稱、方法名稱、參數與傳回值型態,以及相關類別建構語法;在更進階應用場合中,例如想達成任意g(f(x))的函數組合,可定義compose方法:
Func<A, C> compose(final Func<A, B> f, final Func<B, C> g) {
return new Func<A, C>() {
public C apply(A x) {
return g.apply(f.apply(x));
};
};
}
return new Func<A, C>() {
public C apply(A x) {
return g.apply(f.apply(x));
};
};
}
匿名內部類別語法會使得語法冗長到難以理解,令開發者無法專心以函數角度來思考問題。若採用JDK8即將導入的lambda語法,情況就得以改善。先前表達x -> x * 2的例子只要使用(Integer x) -> x * 2,而compose方法的傳回值實作,只要傳回x -> g.apply(f.apply(x)),想將f(x) = x + 2與g(y) = y * 3組合為g(f(x)),可使用compose((Integer x) -> x + 2, (Integer y) -> y * 3),相較於使用匿名內部類別語法,使用lambda語法的版本,確實易於鼓勵開發者以函數角度來思考問題。
不過(Integer x) -> x * 2有點問題,由於Java是靜態語言,變數帶有型態資訊,這使得JDK8的lambda語法基本上必須指定參數型態,不過可透過編譯器的類型推斷(Type inference)來改善,問題是類型推斷的來源為何?先前草案曾打算採用「#傳回值型態(參數型態,...)」的語法來宣告,但這會在現有程式庫創造出lambda語法專用API,還會建立起如##int(int)(int)的複雜語法,這彷彿看到JDK5為了語法簡潔性而引入泛型(Generics),反造成了Enum<E extends Enum<E>>之類的複雜語法;JDK8後來採用單一抽象方法的函式介面(Functional interface),以介面的方法簽署作為類型推斷來源,因此若有個doSome方法參數為Func<Integer, Integer>,就可以使用doSome(x -> x * 2)來呼叫,因為編譯器可從Integer apply(Integer x)推斷型態資訊,省略了lambda語法的參數型態宣告。
- 搭配lambda語法的程式庫
無論程式語言是一開始就支援,或是日後才導入lambda語法,不同程式語言對lambda語法提供不同程度的支援,因而支援lambda語法時必須搭配另一重要主角:可搭配lambda語法的程式庫,因為無法從lambda語法得到的支援,往往可透過程式庫實作來盡可能補足,即使是以lambda演算為基礎的函數式語言,以各種函式為基礎組裝而成的程式庫,也是搭配一級函式時必要的元件。
例如鞣製(Curry)目的之一,是可不用事先宣告,從即有的函式中產生新函式。如果語法上直接支援,使用鞣製特性的開發者就不用親自實作動態產生函式定義的演算法;對於語法上沒有直接支援鞣製的語言來說(像JavaScript),就必須有開發者實作動態產生新函式的API,使用API的人才可享有鞣製特性的好處,複雜演算則交由API開發者負責。
JDK8在導入lambda語法時就考慮到搭配的程式庫,意義之一無非就是讓JDK8使用者不用摸索太久,就可享受到lambda語法的牛肉,另一意義也是在教育開發者,要怎麼在十幾年來都沒有lambda語法的Java中,善用lambda進行程式開發。
在物件導向為主軸的Java中,物件絕對是抽象化的重點,然而許多問題其實都是資料處理問題,例如面對關聯式資料庫中的資料,多數處理無非就是將資料過濾、映射為另一筆資料,然後再作某種型式的處理。在沒有lambda語法的過去,多數開發者習慣以物件觀點來思考,ORM(Object-Relational Mapping)框架也曾盛行一時,然而對於資料庫查詢而得的群集(Collection)資料,也許只需要以函式方式進行處理。
JDK8搭配lambda語法的重要程式庫之一就是改進後的群集框架,其提供了filter、map、reduce等方法,許多群集資料的處理方式,都可由這些方法組合而成,搭配lambda語法使用,更可提高程式的表述能力。比方說blocks若為List<Block>型態,取得所有紅色積木的重量總合可寫為:
int sum = blocks.filter(b -> b.getColor() == RED)
.map(b -> b.getWeight())
.sum();
.map(b -> b.getWeight())
.sum();
- 解決特定問題、增加表述能力、隱藏低階細節
物件導向語言本身就可用來解決問題,納入lambda語法的目的之一,就是為原本語言提供小型通用語言,讓使用物件導向解決時會導致複雜語法或設計的問題,可以使用lambda語法優雅地解決,可預見地,除了JDK本身程式庫將搭配lambda語法而演化,相關開放原始碼或許也將呈現不同風格。
由於程式庫的封裝,平行化、函數式等方向的可能性,也將更簡單且更具表述性,以群集框架新增的filter、map、reduce等方法為例,其內部也許會採遞迴、迭代,或基於效能採用延遲或平行化等更複雜的演算法,然而由於這一切都被封裝在程式庫中,使得Java開發者無需面對複雜演算,因而在解題思路可有所不同,少了複雜語法與演算的雜訊後,開發者就可進一步思索map、filter、reduce等解決問題的基本形式。