所以函數式編程是什麼(二)

閱讀時間約 5 分鐘

前一篇文章中所提的函數式的三個機制明確說明了它關注的規則與能力具體是什麼。然而這套對於函數式編程的定義主要基於特定的類型系統,作為一個編程範式來說過於狹隘(物件導向的定義也是這樣)。更廣義的,我認為函數式編程主要依循三個原則,它們可以應用於任何程式語言,就算沒有靜態類型系統的支援也可以。例如在不管類型的情況下,有些人認為函數式編程就是盡量減少修改和副作用,它就是其中一個原則的實現。這三個原則分別是:最小能力原則、參考透明原則、單一情境原則,它們從方法論上形塑了函數式編程的風格。


最小能力原則(least power principle)認為定義資料結構、函式、介面、變數等實體時,應該盡量縮減它們的能力。能力越強責任越大,因此過強的能力會讓我們需要考慮的東西變多。能力越弱,就必須藉由實體間的合作完成任務,因此如果能落實這個原則,能力越小反而可以提升這些實體的用處。能力越小也有助於分化職責,並提升程式碼的重用性。更重要的是,透過思考如何縮減能力,我們可以更容易將有意義的耦合與非必要的耦合分離出來,從而得到具有同調性的模型。這個原則在函數式編程的應用範圍非常廣泛,小至一段程式碼,大到整個程式語言的設計,都能看到最小能力原則的影子。很多編程建議都可以歸結於這個原則,例如:宣告變數時應該優先考慮宣告成不可變的、應盡量縮減變數的作用域、函式應該只做一件事並做到最好、iterator/pipeline pattern、介面應該盡量細分。函數式編程與物件導向機制上的差別也能體現這個原則,例如子類關係、實體類型的檢查、類別=資料+介面、介面的繼承等,都是物件導向中相對於函數式編程強大的機制,但這反而讓程式語言在某些層面上更糟了,例如:難以描述具體類型、泛型失去同調性、出現反直覺的多型實作。最小能力原則著重於能力而非模型本身,因此我們常需要解構模型背後的邏輯,而非直接依循模型本身設計程式,這點與物件導向非常不一樣。


參考透明原則(referential transparency principle)認為程式應該盡量參考透明。參考透明性指的是表達式的可取代性:如果一個變數可以像巨集一樣直接取代成它的定義,而程式行為不會因此改變,那麼我們說它是參考透明的。這說明操作的順序、執行次數不應影響結果。反之會互相影響的操作就是不透明的,影響越大則越不透明。變數的修改就是不透明的操作,改變它的順序會影響程式的行為,所以應該盡量減少使用變數修改。更廣義的,如果一個操作可以很容易看出他造成什麼影響,那麼它也應視作參考透明的。例如Java的例外處理必須明確標示於定義上,因此這部分在某種程度上也算是蠻透明的,但是呼叫此函式時無法從語法上判斷是否可能有例外丟出。操作之間的影響應該只由依賴關係決定,如果一個操作使用了前一個操作的結果,那麼它們就有依賴關係,因此它會受到前一個操作影響。如果操作之間可以透過其他機制互相影響,那麼可以說它們有隱性的依賴關係。透過揭發這些隱性的依賴關係,可以提升程式的參考透明性。越有參考透明性的程式碼越容易閱讀與重構,只要透過依賴關係就能了解它們之間是如何互相影響。參考透明原則強調操作之間的依賴關係,暴露隱含的依賴關係會讓程式看起來更複雜,但如果不這麼做反而會使程式更容易出錯,更容易出現詭異的相互作用。


單一情境原則(single context principle)認為變數、類型、介面等實體的定義與應用應該盡量限定於單一情境上。定義類型時它的行為是根據其意義所決定的,而情境越單一,語義就越明確。一般用途的類型包含很多相關函式,但並不是所有的函式都適用於所有情境下,例如位元移位運算子並不適用於表示數量的變數,就算你只是打算乘以二也不該這麼做。一般來說,我們可以藉由浮點數描述某個物件的實際長度,但前提是我們必須知道你指的是在哪個單位下。如果只是在小範圍使用,我們可以在變數上標註單位以描述情境。但如果它和多個函式相關,最好定義新的類型包裝起來,並在此類型的說明文件上描述使用情境。透過將浮點數包進一個類型,我們可以賦予實際長度的語義給這個數字,讓這個變數的使用情境更明確。我們常用整數的正負號當作比較順序的結果,更好的方式應該是定義專用的列舉表示順序。例如rust定義Ordering以表示比較順序,同時還能提供串聯和反轉等輔助方法,這歸功於更明確的類型和語義。類似的,findIndex的方法常以回傳負數代表找不到,這也是把整數用做多個情境的問題,這種條件式情境可以透過代數資料類型描述,例如可以像rust使用Option明確將不同的狀況分離開來。如果狀況很複雜以至於無法單純用代數資料類型描述,就必須寫一長串說明文字描述可能的狀況和應該要怎麼使用,這時我們應該透過類型將各種情境分離開來,盡量讓不合法的使用方式變得看起來不合理,就像電腦上的各種接線都有不同的形狀一樣。物件導向則傾向將複雜的狀況封裝起來,並用動態的方式管理狀態,但這樣會使其缺乏彈性,而且很容易會為了合法性重複檢查相同的判斷式。物件導向定義類別往往是為了封裝,但這個原則認為應該要為了描述情境和語義而定義類型,並透過類型說話而非文件。總結來說,單一情境原則專注於語義的情境,透過分離情境可以讓語義更明確,使得不合法的狀況看起來不合理。


我認為這三個原則可以描述函數式編程的風格,第二個是我自己延伸自參考透明的特性,第三個是我從類型驅動編程抽取出來的概念。這套定義或許和多數人對於函數式編程的印象很不一樣,尤其是在javascript學函數式編程的人,但這就是我總結我使用函數式程式語言的經驗所得到的結論。


3會員
23內容數
這不是教你如何從物件導向到函數式編程的入門教程。我會深入探討物件導向與函數式編程的差異,並討論為什麼你應該使用函數式編程並徹底放棄物件導向。
留言0
查看全部
發表第一個留言支持創作者!