2016-09-29 72 views
9

我是一名高級C/C++/Java/Assembler程序員,我一直對純函數式編程範例感興趣。我會不時試着實現一些有用的東西,比如一個小工具,但是我經常會很快到達一個地步,我意識到我(和我的工具)在非純語言中會快得多。這可能是因爲我掌握了大量的命令式編程語言經驗,在我的腦海中有成千上萬的偶像,模式和典型的解決方案。如何在引入狀態時限制代碼更改?

這是其中的一種情況。我已經遇到過好幾次了,我希望你們能幫助我。

我們假設我編寫了一個工具來模擬通信網絡。一個重要的任務是生成網絡數據包。產生是相當複雜的,由數十個功能和配置參數,但最後有一個主功能,因爲我覺得它非常有用,我總是寫下簽名:

generatePackets :: Configuration -> [Packet] 

然而,過了一段時間我注意到如果數據包生成在生成過程的許多子功能中的某一個內部具有某種隨機行爲,那將是非常好的。因爲我需要一個隨機數生成器(和我還需要在代碼中的其他地方),這意味着手動更改幾十個簽名來像

f :: Configuration -> RNGState [Packet] 

type RNGState = State StdGen 

我理解這背後的「數學」必要性(沒有國家)。我的問題是在更高層次上:經驗豐富的Haskell程序員如何處理這種情況?什麼樣的設計模式或工作流程可以在以後避免額外的工作?

我從來沒有與經驗豐富的Haskell程序員合作過。也許你會告訴我,你永遠不會寫簽名,因爲你必須經常改變它們,或者你給所有的函數一個狀態單元,「以防萬一」:)

+4

我不認爲有什麼辦法可以避免這個......而且這個*很好*。將某些東西從純粹的變成不純的**是一件大事,並且讓代碼無聲地編譯這種變化意味着你幾乎完全放棄了類型安全...... – Bakuriu

+1

如何在配置中添加隨機種子? – immibis

+1

我想知道這個問題是否更適合程序員.stackexchange.com,因爲這是一個關於軟件開發實踐的問題。 – chepner

回答

9

一種方法,我一直很公平成功與使用monad變壓器堆棧。這可以讓你在需要的時候添加新的效果,並追蹤特定功能所需的效果。

下面是一個非常簡單的例子。

import Control.Monad.State 
import Control.Monad.Reader 

data Config = Config { v1 :: Int, v2 :: Int } 

-- the type of the entire program describes all the effects that it can do 
type Program = StateT Int (ReaderT Config IO)() 

runProgram program config startState = 
    runReaderT (runStateT program startState) config 

-- doesn't use configuration values. doesn't do IO  
step1 :: MonadState Int m => m() 
step1 = get >>= \x -> put (x+1) 

-- can use configuration and change state, but can't do IO 
step2 :: (MonadReader Config m, MonadState Int m) => m() 
step2 = do 
    x <- asks v1 
    y <- get 
    put (x+y) 

-- can use configuration and do IO, but won't touch our internal state 
step3 :: (MonadReader Config m, MonadIO m) => m() 
step3 = do 
    x <- asks v2 
    liftIO $ putStrLn ("the value of v2 is " ++ show x) 

program :: Program 
program = step1 >> step2 >> step3 

main :: IO() 
main = do 
    let config = Config { v1 = 42, v2 = 123 } 
     startState = 17 
    result <- runProgram program config startState 
    return() 

現在,如果我們想添加另一種效果:

step4 :: MonadWriter String m => m() 
step4 = tell "done!" 

program :: Program 
program = step1 >> step2 >> step3 >> step4 

只要調整ProgramrunProgram

type Program = StateT Int (ReaderT Config (WriterT String IO))() 

runProgram program config startState = 
    runWriterT $ runReaderT (runStateT program startState) config 

總之,這種做法讓我們在跟蹤的方式分解程序效果,還可以根據需要添加新的效果,而不需要大量的重構。

編輯:

它來到我的注意,我沒有回答有關如何爲已在編寫代碼做些什麼的問題。在許多情況下,這不是太難以改變純代碼爲這種風格:

computation :: Double -> Double -> Double 
computation x y = x + y 

成爲

computation :: Monad m => Double -> Double -> m Double 
computation x y = return (x + y) 

此功能現在對任何單子的工作,但沒有獲得任何額外的效果。具體來說,如果我們添加另一個單子變壓器到Program,那麼computation仍然可以工作。

+1

此代碼假設要修改的類型已經是monad堆棧;似乎還有很多重構要將'StateT StdGen'應用於'[Packet]'。 – chepner

+1

這是一段很好的代碼片段,但我不相信這會回答這個問題。也就是說,如果他的大量代碼已經寫入不同,他/她應該怎麼做?答案不能「始終只是從一個monad堆棧開始」 – naomik

+2

@naomik:是的,這就是我的意思(半開玩笑地說)「給你的所有函數一個狀態monad,以防萬一」,我想知道如何「真正的FP人」處理這個問題。如果我的Haskell程序到處都有狀態,那麼與不純語言的區別就成了語法問題。 – trunklop

相關問題