前言
為了清楚起見,請記住,副作用不是必需的壞事,有時副作用是有用的(尤其是在函數式編程範式之外)。
今天聊一聊函數式編程中的隔離思想,它所想隔離的就是「副作用」
我們先從其他角度來聊一聊副作用這個概念。
生活中的副作用
如果我聽到副作用這個詞後,第一反應是吃藥 。
老話說是藥三分毒,其中三分毒則為副作用。就比如你感冒了,吃了一些西方某些國家研製的專利藥品,然後感冒好了,但是感冒好了之後發現自己禿頂 了。
那麼可以說禿頂就是這個感冒藥的副作用。
我們來捋一下這個邏輯
感冒好沒好? 答:好了這藥算不算感冒藥 ? 答:算感冒藥不吃這個藥的話感冒就不會好,吃不吃 ? 答:吃副作用可不可以忍受 ? 答:至少本來就沒頭髮可以忍上面的副作用有些誇大其詞了,但是藥物一般來說都會有一些副作用。
那麼話說回來,程序中呢?
程序的副作用是什麼
在I/O模型中,我們希望在在I到O之間只有計算,如果中間包含且不僅包含觸發了其他I/O、與此次I -> O計算並不相關的任何事情,都稱為副作用。
為什麼稱之為副作用這樣的詞語呢,「副作用」這個單詞給人第一感覺是糟糕的,從而想讓你警惕起來。如果在I/O之間發生了一些我們不知道的副作用,那麼我們將無法控制住這個過程,測試過程也會變得非常複雜。
可以想像,如果在I/O之間如果要訪問資料庫,則必須確保資料庫正在運行。如果要寫入文件,則必須確保該文件存在並已打開。所以會導致執行過程和測試過程變得很複雜,並不是單一的點對點。
寫到這裡讓我不禁想起了PromiseA+規範的測試用例,官方提供了872種測試用例,你所實現的Promise必須全部通過872種測試用例才符合官方規範。
無副作用的優勢
如果一個I/O模型之間沒有副作用的話會有什麼樣的優勢呢?我們參照最開始生活角度的那個例子。
如果感冒藥換成某東方國家生產的無副作用的藥品,在我們感冒的時候,吃感冒藥,感冒好了。過程中無任何副作用的產生,不會禿頂。
那麼我們就可以放心的在感冒的情況下吃這種藥品,而不用考慮其他情況。這就是一個純的I/O模型。
在編程中,我們聲明了一個函數double,如下
在每次輸入x = 3的情況下,double返回恆等與6。它不依賴於我們傳遞它的參數之外的任何東西。
可以想像,在我們調用double的時候,地球上發生著各種各樣的事情,如果在調用的瞬間,天上出現了奧特曼,double依然輸出6。在固定輸入的情況下它是永恆的。
當你寫下這個函數之後,你的餘生都可以放心使用它,無論上下文如何,它將永久有效。
永恆的東西變化的頻率較低,測試起來更加容易,調試起來更加容易。這就是為什麼現在很多程式語言都傾向於無副作用。
函數式編程中的副作用概念
如果函數有副作用,我們將其稱為過程
函數式編程是基於沒有副作用的這樣一個簡單的前提。在這種範例中,副作用是被排斥的。
如果函數有副作用,我們將其稱為過程,或者命令式。因此函數沒有副作用。我們認為,如果函數修改了可變數據結構或變量,使用I/O,引發異常或中止錯誤,則將產生副作用。所有這些東西都被認為是副作用。
副作用之所以不好,是因為(如果有)副作用,取決於系統狀態,功能可能是不可預測的。當一個函數沒有副作用時,我們可以隨時執行它,在給定相同的輸入的情況下,它將始終返回相同的結果。
但是要聲明一點,函數式編程並不是不需要副作用,只是在需要時限制它們。
需要有副作用,因為沒有它們,我們的程序將只能進行計算。
我們經常必須寫資料庫,與外部系統集成或寫文件。與外界通過接口的形式交互才能將我們的計算展示出去。所以很多傾向無副作用的語言的中心細想是把「作用」與「副作用」分離開來處理。
下面我們通過一些特性來看一下。
參照透明
對於同一輸入總是返回相同結果的函數稱為純函數。因此,純函數是沒有可觀察到的副作用的函數,如果函數有任何副作用,即使我們使用相同的參數調用它,也可能返回不同的結果。所以我們可以將純函數替換為其計算值,例如:
如果我們輸入x = 2, y = 2,那麼我們可以得到 4 = sum(2, 2)。
那麼sum為純函數嗎?很顯然是的,如果我們恆定傳入x = 2, y = 2。那麼sum將恆定輸出4.
那麼意味著 f(2, 2) 可以替換為4,比如 Math.floor(sum(2, 2)) 替換之後 Math.floor(4),是一致的。
它就像一個很大的查詢表。我們可以這樣做是因為它沒有任何副作用。用其計算值替換表達式的能力稱為參照透明性。
引用透明很重要,因為它允許我們用值替換表達式。此屬性使我們能夠使用替換模型來思考和推理程序評估。因此,可以說可以用值替換的表達式是確定性的,因為它們始終為給定的輸入返回相同的值。
局部副作用
在講局部副作用之前,我們先來舉一個非局部副作用的典型例子。
上述的factorial函數有副作用嗎?
答:很顯然是有的。函數內部與外界產生了可見的交互,外界result值在函數內部被修改了。而且第一次調用factorial(2) 返回值為 3,第二次調用返回值為6。對於統一輸入不能總返回同一結果。這種副作用是被函數式編程思想所排斥的,與外界的交互使得factorial具有不確定性。
接下來我們看一下另一個例子
那麼問題來了,這次factorial有副作用嗎?
答:有副作用,因為for每次執行的時候都會改變factorial的返回值,result在不斷改變。
但是即使這樣,factorial(2) 也可以用一個值代替,如果把factorial看作一個黑盒子,從外部我們是看不到副作用的。每次的輸入x = 2,總會有固定的返回值3。
換句話說,該函數是具有確定性的,我們說的功能有局部副作用,但此功能的用戶並不關心,因為它沒有破壞我們的替代模式。因此,即使具有局部副作用,該函數也是純淨的。這也是上面為什麼說「產生了可見的交互」,很顯然這句話就是這麼嚴謹,如果見不到,依然是純的。
在函數式編程開發中,可以用一些技巧,比如利用容器,把一些副作用控制在局部以達到純的目的。
舉一些副作用的典型例子
想了想還是在這裡立舉一些典型有副作用的例子,通過例子可以更好的理解這種思想。
1、與外界交互的。
2、調用I/O的
3、從函數範圍之外檢索值
4、磁碟檢索
5、拋出異常