認識 Lambda/Closure(6)一級函式與 Lambda 演算


English

值可以指定給變數。一級值(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 演算來實作控制流程函式,像是 ifforEach 之類的。基本上,可以用 Lambda 演算式來實作一個小型通用式語言。例如,可以如下實作 notandor
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 運算式,如果 condtrue,則會傳回第一個 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 函式在 condtrue 時會傳回第二個 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 + 2g(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 新特性,就能解決這些問題嗎?這就是下篇文章所要探討的了。