函數式編程如何優雅的處理很多 多個函數都要用到的 參數?
面向對象解決的一個比較重要的問題就是很好的封裝了 global constant (variable)這樣子保證了每一個函數中的參數不會過長。
比如我現在有一個最大的函數,其中從 disk 上讀起來一個 config,然後我這個大的函數中有好幾層小的函數,每一個函數和函數的 helper 都要不同程度的調用這個 config。
那麼在面向對象編程中我們就可以把這個 config 設計成一個該對象的 member,這樣我們就不用滿篇的函數中都需要傳入這個 config 參數了
那麼 functional programming 對於這種情況是怎麼解決的呢?
另外一個情況,如果我用c語言來實現fp中的無狀態函數,那也面臨很多參數的問題。那就更抓狂了吧
1. 設計一個 Config 數據類型,包含需要用到的配置。然後所有需要用到該配置的函數,顯式傳 Config 作為函數參數,為方便起見,通常設為第一個參數。函數大概長這樣:
work :: Config -&> IO ()
2. 一個函數實際上就是一個 reader monad。如果函數帶副作用,且使用 mtl 管理副作用,那麼「讀取配置」顯然也算是一項副作用,可以用 MonadReader 來表示,在函數體中可以使用 ask 獲取當前配置,使用 local 修改配置並重啟計算。函數大概長這樣:
work :: (MonadReader Config m, MonadBase IO m) =&> m ()
3. 如果項目複雜,一個 Config 類型不足以用在所有場合,需要多個 Config,可以在 2. 的基礎上使用 classy lens 對 config 類型進行抽象。比如某個函數需要從配置中讀取 SessionId,那麼先實現一個 HasSessionId class,提供從某個類型聚焦到 SessionId 的 lens(也就是 getter/setter 函數),然後函數這麼寫:
work :: (MonadReader env m, HasSessionId env, MonadBase IO m) =&> m ()
現在 work 函數可以傳入不同種類的 Config 類型調用,只要這些 Config 類型包含 SessionId。
4. 在函數式編程中,題主提出的問題叫「configuration problem」。Oleg Kiselyov 提出過一個很有意思的解決方案:Functional pearl: implicit configurations,實現參考 reflection: Reifies arbitrary terms into types that can be reflected back into terms
5. 當然,最方便的還是用全局變數傳配置了,沒有全局變數的編程語言還好意思出來混?https://www.cambridge.org/core/journals/journal-of-functional-programming/article/global-variables-in-haskell/37A6F7551C3A84120D658CE2D2C55E6E 我覺得這個也很優雅。
好問題, 這個問題一直放在我的 problem list 裡面, 雖然有 Oleg Kiselyov 這種問題終結者在這上面發了 paper, 還有 Edward Kmett 根據 paper 寫了個庫, 但是這 paper 一直沒認真看, 庫也沒用過, @Canto Ostinato 也已經介紹得差不多了, 所以沒我什麼事了 (逃
其實寫 Java 的人對這個問題最熟悉不過了, 這就是依賴注入最常見的一個應用場景, 例如寫 Web 服務需要連資料庫打 log 之類的, 拚命把資料庫連接跟 logger 引用傳來傳去肯定蛋疼, 所以把資料庫連接/ logger 看做是種依賴注入到代碼用起來就比較方便.
Haskell 社區在這方面也不少探索:
例如這篇 The Service Pattern, 搞得寫 Haskell 好像寫 ML 一樣 (
A Solution to the Configuration Problem in Haskell 里提到的一些""方案
Using mutable references and some hacking with unsafePerformIO, which always gives the programmer a bad conscience.
Using a Reader monad, requiring a rewrite of the whole program in monadic style.Using implicit parameters which is ok if you did not write type signatures, but if you did, you still have to modify them a lot. Some advanced type hackery.
還有作者用 Template Haskell 魔改代碼的方案.
Scala 社區里有個叫 Cake Pattern 的東西: Scrap Your Cake Pattern Boilerplate: Dependency Injection Using the Reader Monad
如果在 Web 框架裡面 我更傾向用 Reader Monad 的解決方案
加個圖
首先,你需要的是一個「優化「,而不是」設計「,設計應該是直觀明了的,函數形參該怎麼設計怎麼設計,不應該為了「優化」而影響「設計」。這是大的原則。
如果你用Javascript,通常會用到bind來做優化(這個過程也被叫做inversion of control)。
比如有一個比較一般的process函數
function process(config) { ... }
假設另有兩個比較具體的foo和bar函數,也要用到config,但同時也用到別的參數
function foo (config, x) { ... }
function bar (config, y, z) { ... }
你可以
var process_foo = foo.bind(this, config)
var process_bar = foo.bind(this, config)
然後這樣調用
process_foo(x)
process_bar(y, z)
因為process_foo, process_bar的第一個實參被綁定到config了。
這是一個為了讓代碼DRY的「優化」。
其他的語言應該有相近的方法,實際上,越是「純」的函數式語言,這種優化越是好做。
前面有人說為什麼不老老實實傳參數,聽起來雖然土,但其實是有道理的。因為嚴格地說函數資深就是一個基本模塊,他的參數並不存在「復用」的問題,輸入在概念上,應該被認為是unique的。這裡真正需要避免的,其實是全部共享某個量的問題。
我認為最優雅的方式就是把參數傳進去。這樣寫起來很清楚啊
Reflection 那個是當你需要往本來沒有參數的地方傳參數用的啊,比如一個 typeclass instance 依賴於一個值,那就得上 reflection 了
Record 的話,可以這麼著先定義一個 Config
data MahoConfig
= MahoConfig
{ mcName :: String
, mcAge :: Int
, mcOccupation :: String
}
然後打開 -XNamedFieldPuns,表示在等號右側直接把欄位名字對應到欄位值。比寫 Pattern matching 稍微短點
speak MahoConfig{mcName, mcAge, mcOccupation} =
"I"m " ++ mcName ++ ", my age is " ++ show mcAge ++ " and I"m a " ++ mcOccupation
如果某些欄位不需要,不用寫出來
speakNoName MahoConfig{mcAge, mcOccupation} =
"My age is " ++ show mcAge ++ " and I"m a " ++ mcOccupation
或者乾脆打開 -XRecordWildcards,寫個省略號引入所有欄位
speakWild MahoConfig{..} =
"I"m " ++ mcName ++ ", my age is " ++ show mcAge ++ " and I"m a " ++ mcOccupation
誰說函數式編程不能傳遞一個對象的?
為什麼不老實去傳參數
寫個宏,不就解決了嗎?
Scala 的話可以使用 implicit 傳遞隱式值。
當然其實C類語言寫宏很舒服呀!同學,你需要的是 Reader Applicative
不知道題主用不用SML或者OCaml?
用的話,果斷入module system大坑啊(逃
推薦閱讀:
※如何掌握函數式編程?
※以函數編程語言作為入門的編程語言有什麼好處?
※聲明式編程和命令式編程有什麼區別?
※大家對於徐昊的《對象已死?》這篇文章怎麼看?