tru*_*lop 9 haskell functional-programming
我是高级C/C++/Java /汇编程序员,我一直对纯函数式编程范例着迷.我不时尝试用它来实现一些有用的东西,例如,一个小工具,但我经常很快就会意识到我(以及我的工具)在非纯语言中会更快.这可能是因为我对命令式编程语言有更多的经验,我的头脑中有成千上万的idoms,模式和典型的解决方案.
这是其中一种情况.我已经好几次遇到它了,我希望你们能帮助我.
让我们假设我编写了一个模拟通信网络的工具.一个重要的任务是生成网络数据包.这一代非常复杂,由许多函数和配置参数组成,但最后有一个主函数,因为我发现它很有用,我总是记下签名:
generatePackets :: Configuration -> [Packet]
Run Code Online (Sandbox Code Playgroud)
但是,经过一段时间后,我注意到,如果数据包生成在生成过程的众多子函数之一中存在某种随机行为,那将会很棒.由于我需要一个随机数生成器(我也需要在代码中的其他位置),这意味着手动将几十个签名更改为类似的
f :: Configuration -> RNGState [Packet]
Run Code Online (Sandbox Code Playgroud)
同
type RNGState = State StdGen
Run Code Online (Sandbox Code Playgroud)
我理解这背后的"数学"必要性(没有状态).我的问题是更高的(?)级别:经验丰富的Haskell程序员将如何处理这种情况?什么样的设计模式或工作流程可以避免以后的额外工作?
我从未与经验丰富的Haskell程序员合作过.也许你会告诉我你永远不会写签名,因为你之后必须经常更改签名,或者你给所有的功能都是状态monad,"以防万一":)
我相当成功的一种方法是使用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 ()
Run Code Online (Sandbox Code Playgroud)
现在,如果我们想要添加另一个效果:
step4 :: MonadWriter String m => m()
step4 = tell "done!"
program :: Program
program = step1 >> step2 >> step3 >> step4
Run Code Online (Sandbox Code Playgroud)
只是调整Program
和runProgram
type Program = StateT Int (ReaderT Config (WriterT String IO)) ()
runProgram program config startState =
runWriterT $ runReaderT (runStateT program startState) config
Run Code Online (Sandbox Code Playgroud)
总而言之,这种方法允许我们以跟踪效果的方式分解程序,但也允许在不需要大量重构的情况下根据需要添加新效果.
我注意到,我没有回答有关如何处理已经编写的代码的问题.在许多情况下,将纯代码更改为此样式并不困难:
computation :: Double -> Double -> Double
computation x y = x + y
Run Code Online (Sandbox Code Playgroud)
变
computation :: Monad m => Double -> Double -> m Double
computation x y = return (x + y)
Run Code Online (Sandbox Code Playgroud)
此功能现在适用于任何monad,但无法访问任何额外的效果.具体来说,如果我们添加另一个monad变换器Program
,那么computation
仍然可以工作.