如何在引入状态时限制代码更改?

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,"以防万一":)

use*_*560 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 ()
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)

只是调整ProgramrunProgram

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仍然可以工作.

  • @naomik:是的,这就是我的意思(半开玩笑地),"将你的所有功能都设置为状态monad,以防万一",我想知道"真正的FP-guys"如何处理这个问题.如果我的Haskell程序在任何地方都有状态,那么与不纯语言的差异就变成了语法问题. (2认同)