認識 Lambda/Closure(6)一級函式與 Lambda 演算 by caterpillar | CodeData
top

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

分享:

認識 Lambda/Closure(5)Java 的稻草人提案 << 前情

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 新特性,就能解決這些問題嗎?這就是下篇文章所要探討的了。

後續 >> 認識 Lambda/Closure(7)JDK8 Lambda 語法

分享:
按讚!加入 CodeData Facebook 粉絲群

留言

留言請先。還沒帳號註冊也可以使用FacebookGoogle+登錄留言

changyuheng04/11

"public C apply(A, x) {" 這一行中,A 和 x 中間多了一個 ","。

熱門論壇文章

熱門技術文章