Java函數語言程式設計:二、高階函數,閉包,函陣列合以及柯里化

2022-11-03 06:01:39

承接上文:Java函數語言程式設計:一、函數式介面,lambda表示式和方法參照
這次來聊聊函數語言程式設計中其他的幾個比較重要的概念和技術,從而使得我們能更深刻的掌握Java中的函數語言程式設計。
本篇部落格主要聊聊以下幾個問題

  • 高階函數
  • 閉包概念
  • 函陣列合處理常式的使用
  • 柯里化以及部分求值
    下面開始:

1、高階函數

高階函數這裡指的可不是數學裡的那個,這裡主要要從維度這個概念理解,本來函數生成的是值,也就是說,函數比值高維,那麼如果我們有一個函數能生成函數或者是以函數為引數,那麼顯然就比普通的生成值的函數更高維,因為我可以生成你。

定義:高階函數是一個能接受函數作為引數能夠把函數作為返回值的函數。

public interface Function{
    String str(String s);
}

public class Procudure{
    // 下面就是一個標準的高階函數
    public Function(String s){
        return s -> s.upperCase();
    }
}

這裡有兩點:

  • 我們可以通過繼承java.util.function中的介面,或是自定義一個函數式介面來為專門的介面建立別名
  • 有了lambda表示式,很明顯,我們很輕鬆就能建立並返回一個函數

但是這只是基本的,還記得函數語言程式設計的意義嗎?這裡的關鍵在於,有時候,我們可以根據接受的函數,讓高階函數生成一個新的函數。

public class test {
    public static Function<String, String> transform(Function<String, String> f){
        return f.andThen(String::toUpperCase);
    }

    public static void main(String[] args) {
        Function<String, String> transform = test2.transform(str -> {
            return str.substring(0, 2);
        });

        String s = transform.apply("abcdefg");
        System.out.println(s);
    }
}

可以看到,這裡我們通過andThen()這個Function介面的通用方法,連線了前後兩個方法,並且使得無論我們輸入了什麼,都會將該字串轉化為全大寫,後面我們輸入了一個擷取前兩個字元作為返回值的方法,但很明顯,這裡可以有更多的選擇,並且我們實際上也可以通過方法參照來參照某些定義好的函數,非常靈活。


2、閉包

什麼是閉包?

考慮一個lambda表示式,它使用了其函數作用域之外的變數。當返回該函數時會發生什麼呢?也即,當我們通過呼叫lambda表示式產生的匿名方法參照這些外部變數會發生什麼呢?

如果一門語言能夠解決這個問題,我們就認為該語言是支援閉包的,或者也可以說它支援詞法作用域。

這裡還涉及到一個術語:變數捕獲

上面聽起來是不是不明白,沒關係,給個例子:

public class Example{
    IntSupplier plus(int x){
        int y = 1;
        return () -> x + y;
    }
}

考慮這個類和其中的方法plus(int x),你會不會發現有一些問題。

因為我們的plus(int x)方法返回的是一個函數,這裡假設返回的函數是f(int x),也就是說,f(int x)返回時,plus(int x)已經執行結束,所以其中的變數int y = 1;已經脫離了作用域,那麼等到我們獲取了f(int x)的物件再呼叫到f(int x)方法時,這個y要怎麼辦呢?

你會發現,上面的這個方法是可以被編譯執行成功的,但是下面的這個就不行:

public class Example{
    IntSupplier plus(int x){
        int y = 1;
        return () -> x + (++y);
    }
}

為什麼呢?編譯器提示:lambda 表示式中使用的變數應為 final 或有效 final

這句話就說的很明白了,對於第一個例子,我們的y雖然沒final關鍵字,但它是事實上的final變數,一旦這裡賦值就不會再改動,而對於第二個方法來說則相當於把y賦予了新的值。

這裡如果我們使用的是參照,比如下面這個例子

public class Example{
    IntSupplier plus(int x){
        Queue<Integer> y = new LinkedList<>();
        y.offer(1);
        return () -> x + y.poll();
    }
}

注意,這裡是可以通過編譯的,因為實際上我們只需要保證這個參照所指向的物件不被修改,避免後面呼叫返回的函數時卻突然發現找不到對應的物件即可。

所以,Java提供的閉包的條件是,我們必須要能夠保證,被捕獲的變數是final

不過要注意的是,這裡如果是多執行緒情況的話,不能保證執行緒安全。


3、函陣列合

之前我們有提到andThen()這個方法,這些方法在Java.util.function包中的各個函數式介面中各有提供,總的來說有這麼幾種:

  • andThen(arg)
    • 先執行原始操作,再執行引數操作
  • compose(arg)
    • 先執行引數操作,再執行原始操作
  • and(arg)
    • 對原始謂詞和引數謂詞做邏輯與運算
  • or(arg)
    • 對原始謂詞和引數謂詞做邏輯或運算
  • negate()
    • 所得謂詞為原始謂詞取反

在後面的流處理章節,你將會體會到這些函陣列合的力量。


4、柯里化和部分求值

所謂柯里化,就是指將一個接受多個引數的函數轉變為一系列,只接受一個引數的函數,在面向函數程式設計裡這麼做的目的,就跟我們在物件導向程式設計裡需要抽象出介面和抽象類是一樣的,目的就是我們可以通過部分求值來複用這些程式碼。

public class Currying{
    // 未柯里化的函數
    static String unCurried(String a,String b){
        return a + b;
    }
    
    // 柯里化的函數
    static Function<String, Function<String, String>> Curried(){
        return a -> b -> a + b;
    }
    
    // 範例
    public static void main(String[] args) {
        Function<String, Function<String, String>> curried = test2.Curried();

        System.out.println(unCurried("hello ", "World"));

        Function<String, String> firstWord = curried.apply("hello ");

        System.out.println(firstWord.apply("World"));
        System.out.println(firstWord.apply("My friend"));
        System.out.println(firstWord.apply("My love"));
    }
    
    /** 
    	輸出
        hello World
        hello World
        hello My friend
        hello My love
    **/
}

簡單來說就是每一層都返回下一層的函數,直到最終返回我們需要的值為止。