1. 程式人生 > 實用技巧 >函式式的思考,學習lambda和Stream

函式式的思考,學習lambda和Stream

在前面的章節我們快速學習了lambda和Stream,本章節中我們來回顧和理解函數語言程式設計的思想。
我們不斷的提及函式式這個名詞,它指的是lambda嗎?如果是這樣,採用函數語言程式設計能為你帶來什麼好處呢?

函式式的思考

指令式程式設計

一般我們實現一個系統有兩種思考方式,一種專注於如何實現,比如下廚做菜,通常按照自己熟悉的烹飪方法:首先洗菜,
然後切菜,熱油,下菜,然後…… 這看起來像是一系列的命令合集。對於這種”如何做”式的程式設計風格我們稱之為指令式程式設計,
它的特點非常像工廠的流水線、計算機的指令處理,都是序列化、命令式的。

CookingTask cookingTask = new CookingTask();
cookingTask.wash();
cookingTask.cut();
cookingTask.deepFry();
cookingTask.fried();
...

宣告式程式設計

還有一種方式你關注的是要做什麼,我們如果用lambda和函式式來解決上述問題應該是這樣的:

public class CookingDemo {
    public void doTask(String material, Consumer<String> consumer) {
        consumer.accept(material);
    }
    public static void main(String[] args) {
        CookingDemo cookingDemo = new CookingDemo();
        cookingDemo.doTask("蔬菜", material -> System.out.println("清洗" + material));
        cookingDemo.doTask("蔬菜", material -> System.out.println(material + "切片"));
        cookingDemo.doTask("食用油", material -> System.out.println(material + "燒熱"));
        cookingDemo.doTask("", material -> System.out.println("炒菜"));
    }
}

這裡我們將烹飪的實現細節交給了函式庫,它最大的優勢在於你讀起來就像是在問題陳述,採用這種方式我們很快可以理解它的功能,
當你在烹飪流程中新增其他步驟也變得非常簡單,你只需要呼叫doTask方法將材料傳遞進去處理,比如在食用油燒熱前我要打個雞蛋

cookingDemo.doTask("雞蛋", material -> System.out.println(material + "打碎攪拌均勻"));

而不用再編寫一個處理雞蛋的方法。

什麼是函數語言程式設計

對於“什麼是函數語言程式設計”這一問題最簡化的回答是“它是一種使用函式進行程式設計的方式”。

每個人的理解都是不同的,其核心是:在思考問題時,使用不可變值和函式,函式對一個值進行處理,對映成另一個值。

不同的語言社群往往對各自語言中的特性孤芳自賞。現在談Java程式設計師如何定義函數語言程式設計還為時尚早,
但是,這根本不重要!我們關心的是如何寫出好程式碼,而不是符合函數語言程式設計風格的程式碼。

我們想象一下設計一個函式,輸入一個字串型別和布林型別引數,輸出一個整形引數。

int pos = 0;
public Integer foo(String str, boolea flag){
    if(flag && null != str){
        pos++;
    }
    return pos;
}

這個例子有輸入也有輸出,同時每次呼叫也可能會更行外部的變數值,這樣的函式我們稱之為是有副作用的函式。

在函數語言程式設計的上下文中,一個“函式”對應於一個數學函式:它接受零個或多個引數,生成一個或多個結果,並且不會有任何副作用。
你可以把它看成一個黑盒,它接收輸入併產生一些輸出,像下面的函式

public Integer foo(String str, boolea flag){
    if(flag && null != str){
        return 1;
    }
    return 0;
}

這種型別的函式和你在Java程式語言中見到的函式之間的區別是非常重要的(我們無法想象,log或者sin這樣的數學函式會有副作用)。
尤其是,使用同樣的引數呼叫數學函式,它所返回的結果一定是相同的。這裡,我們暫時不考慮Random.nextInt這樣的方法,

函式的副作用

當談論“函式式”時,我們想說的其實是“像數學函式那樣——沒有副作用”。由此,程式設計上的一些精妙問題隨之而來。
我們的意思是,每個函式都只能使用函式和像if-then-else這樣的數學思想來構建嗎?
或者,我們也允許函式內部執行一些非函式式的操作,只要這些操作的結果不會暴露給系統中的其他部分?
換句話說,如果程式有一定的副作用,不過該副作用不會為其他的呼叫者感知,是否我們能假設這種副作用不存在呢?
呼叫者不需要知道,或者完全不在意這些副作用,因為這對它完全沒有影響。

當我們希望能界定這二者之間的區別時,我們將第一種稱為純粹的函數語言程式設計,後者稱為函數語言程式設計。

在程式設計實戰中我們很難用Java語言以純粹的函式式來完成一個程式的,因為很多老的程式碼包括標準庫的函式都是有副作用的
(呼叫Scanner.nextLine就有副作用,它會從一個檔案中讀取一行, 通常情況兩次呼叫的結果完全不同)。你希望為你的系統
編寫接近純函式式的實現,需要確保你的程式碼沒有副作用。假設這樣一個函式或者方法,它沒有副作用,進入方法體執行時會對一個欄位的值加一,
退出方法體之前會對該欄位減一。對一個單執行緒的程式而言,這個方法是沒有副作用的,可以看作函式式的實現。

我們構建函式式的準則是,被稱為“函式式”的函式或方法都只能修改區域性變數,除此之外,它引用的物件都應該是final的。
所有的引用型別欄位都指向不可變物件。