Scala以cascade的方式調用函數有什麼不妥嗎?
玩Scala的時候,發現一條語句可以寫的很長,即以cascade的方式進行函數調用,甚至可以不用局部變數直接一條語句完成一個複雜的函數,想知道這種方式除了可讀性不好之外還有什麼不妥?
可能描述的有點不妥,我的意思是像下面這種方式:input.map….reduce….fold…map不斷調用高階函數,不使用局部變數。
找了一圈,在http://twitter.github.io/effectivescala/index-cn.html#集合-風格 看到了答案,應該是出自Twitter工程師的經驗之談,有一定的參考價值。
函數式編程鼓勵使用流水線轉換將一個不可變的集合塑造為想要的結果。這常常會有非常簡明的方案,但也容易迷惑讀者——很難領悟作者的意圖,或跟蹤所有隱含的中間結果。例如,我們想要從一組投票結果(語言,票數)中統計不同程序語言的票數並按照得票的順序顯示:
val votes = Seq(("scala", 1), ("java", 4), ("scala", 10), ("scala", 1), ("python", 10))
val orderedVotes = votes
.groupBy(_._1)
.map { case (which, counts) =&>
(which, counts.foldLeft(0)(_ + _._2))
}.toSeq
.sortBy(_._2)
.reverse
上面的代碼簡潔並且正確,但幾乎每個讀者都不好理解作者的原本意圖。一個策略是聲明中間結果和參數:
val votesByLang = votes groupBy { case (lang, _) =&> lang }
val sumByLang = votesByLang map { case (lang, counts) =&>
val countsOnly = counts map { case (_, count) =&> count }
(lang, countsOnly.sum)
}
val orderedVotes = sumByLang.toSeq
.sortBy { case (_, count) =&> count }
.reverse
代碼也同樣簡潔,但更清晰的表達了轉換的發生(通過命名中間值),和正在操作的數據的結構(通過命名參數)。如果你擔心這種風格污染了命名空間,用大括弧{}來將表達式分組:
val orderedVotes = {
val votesByLang = ...
...
}
我都這麼寫,沒啥,分好行,做好注釋就行。彆強行用,覺得不順就停下來賦個值歇一歇。想想java程序員你就覺得停一下不打緊了
你指的這種 style 是 tacit programming 么?……
我最初寫 Scala 的時候也喜歡寫這種 point-free 的語句。但是後來漸漸放棄了。
可讀性問題只是其一。
更大的問題在於,Scala 很難做到 consistent 的 point-free。比如 Scala 有 method 的 function 的分別,於是寫起來有時候會醜陋。再比如 .apply 作為 function 時必須顯式地寫出,會造成 inconsistent 。若是遇到 implicit argument,就更難寫得 point-free 了。還有就是 Scala 函數調用可以省略 () 這一點,也很容易造成 confusion 和 inconsistency。
所以我在寫 Scala 的時候還是有節制地使用 tacit programming;而不像在寫 Haskell 時那樣 $ 得飛起……
等等,sbt 說編譯完了,我回去寫代碼了,不答題了……主要還是可讀性吧。命令污染和性能沒那麼嚴重。不是特別長的用注釋也可以。
@hellocode 提到的那個例子是word count,熟悉的人第一眼看FP的那段就明白了(題外話,非FP背景的人這裡可能會用for + mutable.Map)。
另外,這種方式叫做cascade?第一次碰到類似的是jQuery的chain call。Java裡面StringBuilder也可以認為是chain call。如果你看到twiiter-util的Future也有chain call。真的可讀性會不好么?這種接龍的使用方式其實並不會長太長的,因為很難想像一個實際問題會有機會讓你接那麼長的龍,畢竟reduce/fold等就會讓你失去繼續長的機會了。
調試不方便
推薦閱讀:
※遊戲的數值系統的實現和演化
※電腦語言「0,1」的蘊涵的數理邏輯知識 到底是什麼樣的?如何後天將其與思維模式結合?
※極樂技術周報(第二十七期)
※FizzBuzz