1. 程式人生 > >Scala 最佳實踐:純函式

Scala 最佳實踐:純函式

我們所處的是一個指令式程式設計(imperative programming)的時代,這也是我們為何更喜歡用命令式風格寫程式碼的原因。在我們周圍的一切都是可變的。雖然可變性並沒有那麼差勁,但是共享可變性就有點麻煩了。當我們引入共享可變性時,各種問題就會隨之而來。函式式風格是應對這類問題的一個很好的方法。

函數語言程式設計指的是僅通過使用純函式(pure function)和不可變值來完成軟體應用的編寫。

在本文,我們將會探討 純函式 的一些內容。

什麼是一個純函式?

純函式沒有任何副作用中文維基:函式副作用),除了它的輸入以外,函式結果不依賴於其他任何事情。

對於給定的輸入,一個純函式唯一的作用是就是產生一個輸出 – 此外無任何作用。

可以將純函式想象為了一個管道,有輸入流入,然後輸出流出,在流入流出的過程中沒有任何損耗。

下面是 Scala 的一個函式,它接收兩個值並返回它們的和:

scala> def add(a:Int, b:Int) = a + b
add: (a: Int, b: Int)Int

這個函式沒有任何的副作用。它不會改變所提供的輸入值,而是利用了另一個純函式,+ 操作符。作為該函式呼叫的結果,它返回了兩個值的和。這個 add 函式就是一個純函式。

當我們使用純函式時,對於函式呼叫的先後順序並無顯式要求。

舉個例子,我們有兩個純函式:加法和乘法,它們接受兩個輸入值,一個返回兩個值的和,一個返回兩個值的積。因為這兩個函式是純函式,下面兩個不同順序的函式呼叫所產生的結果是相同的:

scala> def add(a:Int,b:Int) = a + b
add: (a: Int, b: Int)Int

scala> def multiply(a:Int,b:Int) = a * b
multiply: (a: Int, b: Int)Int

scala> add(5,8) + multiply(5,8)
res0: Int = 53

scala> multiply(5,8) + add(5,8)
res1: Int = 53

不過,如果我們的計算涉及對一個非純函式的呼叫,就不能像上面這樣隨意調換順序進行呼叫了。出於優化角度,可以對使用純函式的表示式的呼叫順序進行重新安排,這樣所產生的結果與之前是完全相同的

為什麼要使用純函式

函數語言程式設計的一個主要原則就是寫出核心為純函式的應用,這樣一來,那麼副作用就會只存在於佔比不多的外層結構。

純函式的好處有:

易推斷

這是因為一個純函式,它沒有任何副作用,也沒有隱藏的 I/O 資訊,僅通過檢視它的簽名就能知道這個函式是幹什麼的。

易組合

一個純函式接受一個輸入,然後對輸入進行一些計算,最後返回一個結果。因為“輸出只依賴於輸入”,所以它不會改變周圍的任何事情,這便使得純函式易於組合起來形成簡單的解決方案。

易測試

比起非純函式,純函式要容易測試的多。舉個例子:

scala> def pureFunction(name : String) = s"My name is $name"

pureFunction: (name: String)String

scala> def impureFunction(name : String) = println(s"My name is $name")

impureFunction: (name: String)Unit

如果想要測試函式 pureFunction, 一行程式碼就足夠了:

assert(pureFunction("Shivangi") == "My name is Shivangi)"

而測試 impureFunction 就要複雜得多了,因為我們需要重定向標準輸出,然後在上面進行斷言。

易除錯

因為一個純函式的輸出僅依賴於函式的輸入和演算法本身,在除錯時,根本不用關心函式外部的資訊,所以純函式比非純函式更易於除錯。

易並行

通過函數語言程式設計很容易寫出並行/併發的應用。原因如下:

如果在兩個純表示式中沒有資料依賴,那麼它們的呼叫順序就可以進行調換,或者可以被並行執行而彼此不會相互影響(換句話說,任何純表示式的求值都是執行緒安全的))。

除此以外,純函式還有以下一些特點:

引用透明

引用透明(Referentially transparent)指的是一個表示式或函式可以被相應的數值進行安全替換。對於所有的引用透明值 x,如果表示式 f(x) 是引用透明的,那麼這個函式就是純函式

現在讓我們來看一下到底引用透明是什麼。

引用透明是一個函式屬性,它指的是函式不受臨時的上下文影響,沒有任何副作用。對一個特定的輸入而言,一個引用透明的呼叫可以在不改變程式語義的情況下被它的結果所代替。

比如,輸入 + 3*2 可以被替換為輸入 + 6,因為子表示式 3*2 是引用透明的。

我們為什麼要關心引用透明呢???

引用透明在程式優化中扮演了一個非常重要的角色。如果能夠在編譯期用一個函式或表示式的值來替換該函式或表示式,將會節省執行期的很多時間。

“引用透明” 指的是表示式的值僅依賴於其自身值,而不依賴於其他任何內容。

冪等

冪等(Idempotent)(中文維基:冪等)這個詞有多重含義,不過在這裡我們僅關注它在程式設計上的意義。給定一個值,如果一個函式或操作不論執行多次或僅執行一次,所得結果都是相同的,那麼我們就說這個函式或操作時冪等的。加法函式就是冪等的,它可以被執行任意多次。對於給定的 a 和 b,如果我們呼叫多少次,所得結果都是一樣的。

純函式就是冪等的。給定一個輸入,基於該輸入值,我們呼叫一個純函式一次,會產生一個輸出值。給定同樣的輸入,基於該輸入值,我們呼叫一個相同的純函式多次,所產生的輸出值是與呼叫一次完全相同的。

冪等的好處就是純函式可以被安全地執行任意多次,甚至如果我們不需要該函式結果的話,完全可以跳過不執行。

引用透明說的是一個純函式可以被安全地替換為函式的輸出值。冪等說的是重複計算任意多次是完全沒問題的。這兩個特性組合起來就是說,處理純函式很容易,而且很完全 – 這給程式優化提供了極大的便利。

記憶

記憶(Memoizable)是一個優化技術。它的目的在於以空間換時間,也就是說,通過儲存或快取計算結果來減少計算時間。

只有當給定引數或輸入,函式結果是完全相同的,記憶才變得有意義。顯然純函式具備這個屬性,因此它們很容易進行記憶。

延遲處理

延遲求值(Lazy evaluation)指的是隻有當需要一個表示式的值時,才會該表示式進行求值。如果在程式執行過程中,這個值從來沒有被用到,那麼可能就根本不會對該表示式求值。在 Scala 中,我們可以通過標記一些變數進行延遲處理。

延遲處理的好處就是,我們變得更有效率了,而這種效率的提升並非通過更快地執行程式,而是通過消除我們不需要執行的操作。通過這種消除計算的方式,我們可以變得十分有效率。

總結

純函式 是函數語言程式設計中一個根本的概念。對於一個純函式,你可以立即求值,也可以放心大膽地放在後面求值。此外,因為無論我們求值多少次,何時求值,一個純函式的結果總是唯一的,所以我們可以儲存求值的結果(通過延遲處理標記)並進行重用。還有,如果一個函式沒有任何副作用,對於想要知道該函式是否已經被求值的任何人,方法就是檢視函式結果。函式計算也可以根據需要進行延遲計算。由於引用透明和記憶特性,對於程式優化也非常有幫助。

參考: