每個命令式程序員都應使用的功能性編程原理

有時似乎函數式程序員是完全不同的品種。即使按照程序員的標準,他們似乎也比其他人更討厭。他們使用奇怪的術語,例如“ monad”,“ for-comprehension”和“ lambda”。他們使用的語言不會以分號結尾。而且,無論Java程序員對C ++程序員有多麼不安,這兩個小組至少可以同意Haskell是很奇怪的。但是,如果有受其功能範式語言青睞的編程原則對我們其他人有用呢?事實是,即使對於習慣使用命令式語言的開發人員,函數式編程也可以提供很多東西。實際上,如今許多功能性編程原理在命令式語言中正變得越來越流行(我正在向您介紹Java 8)。
函數式編程原理如何使我受益?
每個開發人員都希望編寫良好,乾淨,可維護,可理解的代碼。例如,面向對象程序設計的流行部分是由於代碼編寫和維護帶來的好處,而這種好處是範式鼓勵開發人員組織其代碼的方式。
函數式編程提供了自己的工具和實踐,它們可以使代碼以命令式編程無法實現的方式變得更加模塊化。模塊化代碼導致易於理解,易於重用和易於測試的代碼。
您可以將這些工具視爲需要將程序的各個部分連接在一起時可以使用的一種粘合劑。命令式編程提供了某些類型的膠水。函數式編程提供了其他功能。我們可以使用更多類型的膠水來改善我們編寫的代碼的整體結構。
尤其是,比起可變性,他們更喜歡不變性,編寫純函數以及使用遞歸來分解問題,這些實踐都可以作爲一種新型膠水,各有千秋。最好的部分?這些是實踐,而不是語言功能,並且無論您使用哪種語言編寫,都可以使用。
不變性
許多函數式編程語言鼓勵不變性,默認情況下通常使值不變。不變性是指防止狀態被修改。變異可以發生在兩個層次上:參考變異和價值變異。當您爲現有變量分配新引用時,將發生引用突變:
var x = { foo:‘bar’ };
var y = x ;
x = { foo:‘baz’ };
控制檯。log(x,y); //打印“ {foo:‘baz’},{foo:‘bar’}”

在此示例中,引用x被突變,但指向的對象沒有突變,因此by指向的值y未更改。
修改現有對象時,會發生值突變:
var x = { foo:‘bar’ };
var y = x ;
X。foo = ‘baz’ ;
控制檯。log(x,y); //打印“ {foo:‘baz’},{foo:‘baz’}”

在這裏,即使y未直接修改,它仍引用與相同的對象x,並且其foo屬性的值已更改。
參考突變和值突變之間的區別是微妙的,但是仍然需要理解。在許多具有編譯時不變性的語言中,引用不變性很容易添加到您的代碼中,但是值不變性則更加困難。例如,在Java中,您可以將引用聲明爲final,但這不會阻止您更改final被引用對象上非值的值,除非這些值也表示爲 final。
不變性是確保代碼解耦的一種低成本方法。作爲開發人員,它使您可以控制如何更改系統中的對象。例如,在多線程程序中,這可能非常有用。由於變異,導致了許多(儘管不是全部)導致代碼不具有線程安全性的錯誤和模糊的邊緣情況。
如果對象和引用被鎖定,那麼您不必擔心爭用情況,即兩個線程試圖同時覆蓋一個值,或者在兩次讀取之間該值意外更改的情況。它還使代碼更易於直觀地調試。讀取代碼的人不必擔心當前正在讀取的代碼外部的源可能會更改特定的值,因爲該值根本無法更改。這些只是不變性可以使您的代碼更安全,更容易推理的幾種方式。
但是,不變性確實要付出代價。根據對象的實現方式和所使用的語言,爲了修改不可變的對象,您可能需要使用實例化對象時要聲明的更改來克隆整個對象。這將導致創建許多對象然後將其丟棄,這可能會更頻繁地觸發垃圾回收。
因此,某些用例(例如遊戲或GUI開發)不適合不可變性。但是,即使在這些特殊環境中,也可以在適當的地方使用不變性,以便從其提供的安全保證中受益。儘管有這樣的謹慎,但如果您正確地構造對象並故意對對象的哪些部分進行更改,則仍然可以在不降低性能的情況下利用更改的性能。例如,樹或鏈接列表以不可變的方式比哈希表或數組列表更容易使用。
不變性改變了我們處理代碼問題的方式。它改變了我們對代碼部分的思考方式,並鼓勵我們以更清潔,更線程安全的方式將它們組合在一起。但是,僅不變性有時似乎比提供幫助更多的是障礙。幸運的是,與其他功能編程原理結合使用時,操作起來更容易。這些原理中的許多原理,例如純函數,都是通過編寫不可變的代碼來啓用的。
純函數
知道函數式編程將重點放在函數上肯定不足爲奇。但是,它們並不是指命令式程序員指“方法”或“過程”的“功能”。相反,“函數”在這種情況下可以追溯到我們在數學課上學到的函數。像是好東西f(x) = x + 1。這些功能很簡單。他們取一個值並返回結果。它們是可預測的和可靠的。最重要的是,它們僅計算結果。函數式編程鼓勵按照數學中函數的方式編寫程序。
這些稱爲純函數。
純函數的最顯着特徵是它們不修改任何狀態。這包括提供給函數的參數上的狀態,全局狀態,甚至是程序本身外部的狀態。函數式程序員喜歡說非純函數確實可以執行他們想要的任何事情,並且無法在調用站點知道調用站點不會產生副作用。一個有趣的例子是調用非純函數可能會在某處發射導彈。當然不太可能,但是如何保證在不親自調查代碼的情況下調用某些任意過程實際上不會執行此操作?如果該功能是純粹的,那麼根據定義它就不能發射任何導彈。
當然,功能純度可能太高。如果未修改任何狀態,則程序可能根本不會運行。因此,純函數應與不變性一樣謹慎使用。
純函數有很多好處。一個重要的特性是稱爲參照透明性。從理論上講,參照透明函數可以將調用站點替換爲調用函數的實際結果,而根本不改變程序的行爲。
換句話說,參照透明函數可確保給定輸入集的給定結果。無論何時調用2,f(x) = x + 1它將始終返回3 x。這意味着該函數不僅不能在調用時改變任何狀態,而且也不能依賴任何可能會改變的外部狀態。引用透明的函數可以輕鬆地緩存其結果。例如,使用純函數時,便可以進行記憶和動態編程。
純函數自然也是線程安全的。因爲沒有狀態發生突變,所以可以由任意數量的並行線程調用一個純函數。實際上,純函數使並行化和併發編程變得輕而易舉。給定兩個不依賴於彼此結果的純函數,您可以按任何順序調用這些函數而不會引起競爭條件。
將函數轉換爲純函數的最簡單方法是將純函數需要的所有狀態作爲函數的參數注入。如果您的函數過於複雜,則可能會有一些缺點,因爲最終可能會導致參數列表過長。
這也凸顯了與OOP範例一起謹慎使用純函數的重要性。對象上的方法具有許多可用狀態,不需要將其作爲參數提供。如果使對象上的成員變量不可變或使用將對象作爲其參數之一的靜態函數,則可以解決此問題(請考慮使用Python)。
遞歸
遞歸-及其精細的子類型-尾遞歸-是幾乎每個程序員都應該熟悉的概念。遞歸在函數式編程中是必不可少的,在函數式編程中,對不變性和純函數的強調使常規for循環充其量只能在一般意義上使用,並且最好不要這樣做。遞歸是一種循環機制,其中函數在循環的每一遍都重複調用自身,而不是依賴於計數器變量。
遞歸的核心概念之一(也是我將其作爲當務之急的程序員使用的提示的原因)是將較大的問題分解爲較小的,自相似的部分。較小的問題更容易理解,也更直觀地解決。這自然可以提高代碼的理解力和可維護性。
每當遇到需要循環的代碼時,請問問自己遞歸是否是執行循環的正確方法。遍歷數組以對其包含的每個值調用函數更適合常規循環,而使用quicksort策略對數組進行排序將是遞歸的最佳選擇。
如果問題允許,請務必記住使用尾部遞歸(假設您的語言支持)。尾遞歸是指遞歸調用是函數結束之前發生的最後一件事,換句話說,它位於尾部位置。此遞歸函數是尾遞歸:

function factorial(x, acc) {
acc = acc || 1; // acc can be omitted when initially invoking factorial()
if (x > 1) {
return factorial(x - 1, acc * x);
} else {
return acc;
}
}
尾遞歸是有益的,因爲它避免了遞歸最大的弱點之一:堆棧溢出。編譯器可以通過以下方式優化尾遞歸調用:它們不會導致堆棧函數指針在每次調用時都越來越深。如果您的遞歸函數可能被調用了數百次,請考慮以尾遞歸的方式編寫函數或使用更常規的循環機制來重寫它。
結論
函數式程序員和命令式程序員之間的鴻溝並不像您想象的那樣大。最終,雙方都可以爲編程世界做出很多貢獻。函數式編程世界熟悉的工具可以用在命令式編程語言中,以使我們的代碼更簡潔,模塊化,更易於維護。
最後,開發這麼多年我也總結了一套學習Java的資料與面試題,如果你在技術上面想提升自己的話,可以關注我,私信發送領取資料或者在評論區留下自己的聯繫方式,有時間記得幫我點下轉發讓跟多的人看到哦。在這裏插入圖片描述

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章