2016-06-25 35 views
0

在遵循一些關於Scala的教程時,想到了這個問題,但是我認爲在函數式編程方面通常會很有趣。FP中的不可變狀態

我不確定FP中不可變性的重要性。我可以想到2種不同的情況:

1)類方法不會返回實際字段,而是返回它們的副本。例如,如果我們有一個類犬,我們需要保持不變,那麼它的功能:代替

getToys() { return new ArrayList(this.toys); } 

getToys() { return this.toys; } 

這種情況使我感覺良好,在第二種情況的客戶端代碼實際上可能會腐蝕物體。我懷疑在於第二種情況:

2)在Scala和大多數其它FP語言,我們喜歡一個遞歸調用:

sum(x: List[Int], acc: Int) { 
    if(x.isEmpty) return acc; 
    sum(x.tail, acc + x.head); 
} 

對傳統的for循環遞增的蓄電池。推理是這個累加器是一個可變變量。

該變量永遠不會暴露在函數之外,爲什麼要關心使它不可變?

編輯:

這似乎是最重要的最佳做法是引用透明 並沒有嚴格的不變性,意思大致是,我們不只要關心可變的狀態,因爲它不是由客戶端代碼發現。然而人們仍然聲稱,即使可變狀態影響局部變量(如我的第一個示例),代碼更具可讀性或更容易推理。

我個人認爲循環比遞歸更具可讀性。

所以真正的問題是:爲什麼使用不可變變量的代碼被認爲更容易閱讀/推理?

+1

關於1)要點是,如果您的數據結構首先是不可變的,則不需要返回副本。 – sepp2k

+0

[掌握不可變數據結構]的可能重複(http://stackoverflow.com/questions/8346119/a-grasp-of-immutable-datastructures) –

+2

索引變量通常不會暴露,你是對的。只要我們談論僅用於迭代的簡單索引,我們就可以保存。這仍然不僅僅是一個最佳實踐,因爲這是您可以安全使用全局索引的唯一方式,並且仍然是線程保存。 – Julian

回答

4

在斯卡拉和大多數其他FP語言中,我們更喜歡遞歸調用...對傳統的循環遞增累加器。推理是這個累加器是一個可變變量。

您認爲遞歸比遞增累加器的傳統循環更受歡迎。我同意功能程序員喜歡遞歸,但不是因爲你的想法。在很多情況下,遞歸只是表達問題的一種自然而簡明的方式。請注意,由於尾部調用優化消除了分配新堆棧幀的開銷,所以性能不是針對遞歸的參數。

您將遞歸比較爲遞增累加器的for循環。大多數程序員會喜歡循環遍歷一個沒有索引的集合(例如,在Scala中爲for (i <- 0 to 10) { .. }或者在Java中爲for (T x : collection) { .. }for構造了一個迭代遍歷集合的摘要,可以更加清晰地傳達您的意圖,並且b)降低錯誤風險(如一次性錯誤)。抽象更進一步,我們可以表達你的總結使用高階倍整數列表的例子:

scala> (1 to 10).foldLeft(0)(_+_) 
res0: Int = 55 

現在回到你原來的問題:爲什麼是不可變的變量,更易於閱讀和推理?這可能會令人驚訝,但程序員也只是人類。人類對於有許多運動部件的系統來說是不好的推理。不變性減少了運動部件的數量,從而更容易推理程序。

1

你說得對,如果引用不能逃避函數,那麼不變性不是最大的擔憂,但還有其他的考慮。命令式循環具有非常通用和不精確的語義,因此大多數人一旦學習了替代方法,就會發現它們過於草率。

例如,命令式循環沒有返回類型。它們的參數的輸入類型不被檢查。許多類型的命令循環迫使程序員不必要地處理與問題陳述無關的索引。只有少數幾種命令循環意味着涵蓋所有可能的情況,因此它們過於通用。你不能創建自己的新型勢在必行的循環,更好地適應你的具體情況。他們幾乎總是必須建立在語言本身。

出於一些奇怪的原因,每個人似乎都把它作爲遞歸和命令循環之間的錯誤選擇,但遞歸實際上是功能程序員帶中最糟糕的工具。除非絕對必要,否則我們會避免它,或者除非算法自然地遞歸表示。有幾十個高階函數可以用來更精確地表達大多數日常代碼,比遞歸更清晰,同時保留了所有的好處,例如Martjin的foldLeft示例。

僅使用類型檢查優勢值得使用foldLeft而不是for循環。空列表可以毫不費力地處理。沒有奇怪的累加器必須使用循環外的作用域進行初始化。無論如何,你不必爲所有那些你不在乎的臨時變量想出奇怪的名字。這只是簡單的清潔。

你可能會說這是一個特殊的簡單情況,但要小心,避免必要的循環可以給你在絕大多數情況下的這種體驗,以及遞歸最終複雜的情況,必要的解決方案通常會也一樣複雜。當你對所有替代方案進行真正的比較時,勢在必行的環路纔會出現,因爲人們對它們很熟悉。