2015-08-19 57 views
1

我有一些軟件設計經驗,現在我正在學習Haskell。在許多現實世界的軟件開發,一個面臨着類似給出的情況,例如,如下所示:如何重構Haskell函數代碼鏈?

想,我有這樣的代碼現在

f1 a b c d = e 
where 
    e1 = f2 b c (f3 a) 
    e2 = f4 d 
    e = e1 + e2 

f2 b c d = n + c + d 
where 
    n = f5 b 

f5 n = n*n 

f3 a = a * 2 

f4 a = a + 3 

,如果我想改變F5,這樣只有在尚未另一個參數,我將不得不將所有的功能鏈改變到f1。 它可以如下所示完成。請注意添加的參數x。

f1 a b c d x = e -- f1 needs to be changed 
where 
    e1 = f2 b c (f3 a) x 
    e2 = f4 d 
    e = e1 + e2 

f2 b c d x = n + c + d -- f2 needs to be changed 
where 
    n = f5 b x 

f5 n x = n*n -- f5 changed (**bang**) 

f3 a = a * 2 

f4 a = a + 3 

這是做這類事情的正常哈斯克爾方式或是否有更好(更哈斯克爾十歲上下)的方式? 我知道API的這種變化會擾亂客戶端代碼,但是如何將影響保持在最低限度,並且有沒有Hasekll的方式?

在更一般的層面上:Haskell在這種情況下表現如何(特別是考慮到它的不變狀態特性)?它在這方面爲開發者提供了什麼?或者,Haskell本身沒有任何作用發揮其作用,而這只是一個難以解決的軟件工程問題(沒有像未來證明)?

對於在單個帖子中詢問多個問題,我表示歉意,但我無法幫助,因爲這些問題是相互關聯的。另外,我找不到類似的問題,很抱歉,如果我可能錯過了。

+3

好了,你可以隨時更改簽名類型和參數列表(你正在寫類型的簽名,不是嗎?),然後看看那裏的編譯器抱怨。修復第一個問題,然後回到第2步,直到所有錯誤都用盡。如果你有一個共同的一套被傳遞到許多功能的參數,它很可能是更好地使新的數據類型來存儲所有這些值,那麼你就可以避開改變函數的參數,而是隻是改變類型的定義。這是一個軟件問題,不是真正的Haskell問題。 – bheklilr

回答

0

你可以做的一件事就是將參數像這樣捆綁到單個參數對象中,就像bhelkir在評論中所建議的那樣。如果向此對象添加新參數,則仍需要更改調用f1的客戶端代碼,並更改該新參數的直接使用者(這裏爲f5),但這些都是不可避免的:某些人必須在某些情況下提供x點,你需要它成爲客戶;你必須以某種方式消費x,否則你爲什麼要添加它?

但是,你可以不要改變像f1f2中介職能,因爲他們可以忽略不關心的新領域。通過使用((->) t)(通常稱爲Reader)的Applicative實例來傳遞此對象,而不是手動執行此操作,您可以獲得一些幻想。這裏是寫的一個方法:

module Test where 
import Control.Applicative 

data Settings = Settings {getA :: Int, 
          getB :: Int, 
          getC :: Int, 
          getD :: Int} 
f1 :: Settings -> Int 
f1 = liftA2 (+) f2 f4 
-- f1 = do 
-- e1 <- f2 
-- e2 <- f4 
-- return $ e1 + e2 

f2 :: Settings -> Int 
-- probably something clever with liftA3 and (+) is possible here too 
f2 s = f5 s + getC s + f3 s 

f3 :: Settings -> Int 
f3 = liftA (* 2) getA 

f4 :: Settings -> Int 
f4 = liftA (+ 3) getD 

f5 :: Settings -> Int 
-- f5 = liftA (join (*)) getB -- perhaps a bit opaque 
f5 = liftA square getB 
    where square b = b * b 

現在,這有它的長處和短處:那是在f1邏輯(即,知道你需要調用f3a)已經進入f3本身,這會發生在最初讀取參數並在將它傳遞給某個輔助函數之前將其刪除的任何函數。這可能比原來更清晰,或者可能會掩蓋f1背後的意圖,具體取決於您的問題域。你可以總是更加明確地寫一個函數,例如通過修改傳入的Settings對象來改變其a字段,然後再傳遞它,就像我以f2爲例。更一般地說,您可以使用任何最方便的樣式編寫任何函數:符號,應用函數或傳入的記錄對象上的普通舊模式匹配。

但最大的好處是,它很容易添加一個新的參數:你只是一個字段添加到Settings記錄,並在需要它的功能閱讀:

module Test where 
import Control.Applicative 

data Settings = Settings {getA :: Int, 
          getB :: Int, 
          getC :: Int, 
          getD :: Int, 
          getX :: Int} 
f1 :: Settings -> Int 
f1 = liftA2 (+) f2 f4 
-- f1 = do 
-- e1 <- f2 
-- e2 <- f4 
-- return $ e1 + e2 

f2 :: Settings -> Int 
-- probably something clever with liftA3 and (+) is possible here too 
f2 s = f5 s + getC s + f3 s 

f3 :: Settings -> Int 
f3 = liftA (* 2) getA 

f4 :: Settings -> Int 
f4 = liftA (+ 3) getD 

f5 :: Settings -> Int 
f5 = liftA2 squareAdd getB getX 
    where squareAdd b x = b * b + x 

注意一切除了data Settingsf5

+0

關於這一點的巧妙之處在於,它與未來API更改的默認值一起工作良好。所以如果你有一個'defaultSettings',你可以讓用戶執行'defaultSettings {getC = 3}'來修改'getC'。最好的部分是,如果您決定添加一個'getE'字段,則'defaultSettings {getC = 3}'代碼仍然有效:) –