值可以指定給變數。一級值(first-class value)可以傳入函式或由函式中傳回。在多數程式語言中,基本型態或物件是一級值,那麼函式或運算式呢(expression)?
在一些語言中,函式是一等公民(first-class citizen)(或者稱為高階函式)。例如,在 認識 Lambda/Closure(1)中我們看過,JavaScript 的函式是物件,也就是說,它們是一級值,可以指定給變數、傳入函式或從函式中傳回。
不過,為什麼一級函式也稱為 Lambda?在回答這個問題之前,我們必須認識一下 Lambda 演算(也可以寫成 λ 演算)。簡單地說,在 λ 演算中,函式是僅帶一個參數的運算式。參數也可以接受帶有一個參數的函式。λ 演算中的函式是匿名的。例如,若將數學函數
f(x) = x * 2
以匿名函式撰寫的話會是:
λ x. x * 2
如果採用 JDK8 最後採用的語法,則可以寫為:
x -> x * 2
也就是說,Lambda 運算式會將 x
映射為 x * 2
。如果要把 2 套用到 x
,則套用的過程會是:
(x -> x * 2)(2)
= 2 * 2
= 4
如果有個函數 g(y) = y - 1
,並且想將 f(x) = x * 2
套用到 y,那麼可如下得到新函數 h(x) = g(f(x))
:
h(x)
= g(f(x))
= f(x) - 1
= x * 2 - 1
使用 Lambda 運算式將上面套用過程重新寫過的話,就會得到新的 Lambda 運算式。
(y -> y - 1)(x -> x * 2)
= x -> x * 2 - 1
在 λ 演算中,函式是運算式,也稱為 Lambda 函式,它是僅帶一個參數的函式。那麼,如何表示數學上具備兩個輸入的函數呢?來想看看
f(x, y) = x * x + y * y
這個函數。如果 a
套用至 x
,就會得到新函數 f(a, y) = a * a + y * y
。可以令 g(y) = f(a, y) = a * a + y * y
。將 b
套用至 y
會得到 g(b) = a * a + b * b = f(a, b)
。也就是說,需要兩個輸入的函數,可以用接受單一輸入的函數,令其傳回另一接受單一輸入的函數來重新打造。如果用匿名形式來撰寫 f(x, y) = x * x + y * y
,則會是如下的形式:
(x, y) -> x * x + y * y
= x -> (y -> x * x + y * y)
將 a
套用至 x
,接著將 b
套用至 y
的話,則會是:
(x -> (y -> x * x + y * y))(a)(b)
= (y -> a * a + y * y)(b)
= a * a + b * b
在 λ 演算中,任何超過一個參數以上的函式,可以由數個單參數的函式依序套用而得。我們也可以使用 Lambda 演算來實作控制流程函式,像是 if
、forEach
之類的。基本上,可以用 Lambda 演算式來實作一個小型通用式語言。例如,可以如下實作 not
、and
、or
:
let not =
* false -> true
* true -> false
let and =
* false value -> false
* true value -> value
* value false -> false
* value true -> value
let or =
* false value -> value
* true value -> true
* value false -> value
* value true -> true
let if = cond -> tv -> fv -> (cond and tv) or (not cond and fv)
上面的 if
函式就像是一些語言中的 if
運算式,如果 cond
是 true
,則會傳回第一個 tv
。例如:
if(true)(a)(b)
= ((cond or fv) and (cond and tv))(true)(a)(b)
=((true and tv) or (not true and fv))(a)(b)
=((true and a) or (not true and fv))(b)
=(true and a) or (not true and b)
= a or (false and b)
= a or false
= a
我們也可以實作一個 unless
函式。
let unless = cond -> fv -> tv -> (cond or fv) and (cond and tv)
unless
函式在 cond
為 true
時會傳回第二個 tv
。例如:
unless(true)(a)(b)
= ((cond or fv) and (cond and tv))(true)(a)(b)
= ((true or fv) and (true and tv))(a)(b)
= ((true or a) and (true and tv))(b)
= (true or a) and (true and b)
= true and b
= b
不同的語言會提供不同的語法來支援 Lambda 運算式。例如,用 JavaScript 來表達 (x -> x * 2)
的話,可以寫成以下形式:
function(x) {
return x * 2;
};
這個語法看起來有點囉嗦。基本上,我們關心的只是 x
會被映射為 x * 2
。也許我們不該太過挑剔,至少 JavaScript 直接提供了一級函式的特性,而且它是個動態語言。如果使用不支援一級函式的靜態語言的話,像是 Java,那會發生什麼事?我們曾經看過,在 Java 中最接近 Lambda 函式的東西是匿名類別。如果想用它來表達
(x -> x * 2)
的話該怎麼做呢?首先,具有一個參數與傳回值的函式,我們使用具有單一抽象方法的介面來定義。
public interface Func<P, R> {
R apply(P p);
}
接著,可以使用匿名類別來實作 (x -> x * 2)
,如下:
new Func<Integer, Integer>() {
public Integer apply(Integer x) {
return x * 2;
}
};
哇喔!好大一垞!匿名類別的語法已經夠惱人了,我們甚至還得宣告型態,因為 Java 是靜態語言。如果我們打算進行函式合成的話,像是 f(g(x))
,可以如下撰寫個 compose
方法:
public static <A, B, C> 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));
}
};
}
你能一眼看出我們打算做什麼嗎?基本上,我們真正需要的只是 g.apply(f.apply(x))
,不過匿名類別的語法讓我們失焦了。如果打算使用 compose
方法來做函式合成 g(f(x))
,而其中 f(x) = x + 2
且 g(y) = y * 3
,那麼就必須如下撰寫程式碼:
compose(
new Func<Integer, Integer>() {
public Integer apply(Integer x) {
return x + 2;
}
},
new Func<Integer, Integer>() {
public Integer apply(Integer y) {
return y * 3;
}
}
);
正如在 認識 Lambda/Closure(5) 中談過的,有關 Java 不需要 Lambda/Closure 的爭論是對,只是相對的代價是,撰寫更多的程式碼。有時候,也許是在多數情況下,我們很難看出程式碼到底想表達什麼,即使只是做個如上 g(f(x))
這樣簡單的函式合成也是如此。使用 JDK8 中帶來的 Lambda 新特性,就能解決這些問題嗎?這就是下篇文章所要探討的了。