我先回答你的第二个问题。在 Haskell(和其他 FP 语言)中,实际上有很多方法可以处理可变状态。首先,Haskell 通过IORef 和mvar 构造支持IO 中的可变状态。使用这些命令式语言的程序员会感到非常熟悉。还有专门的版本,例如STRef 和TMVar,以及可变数组、指针和其他各种可变数据。最大的缺点是这些通常只在 IO 或更专业的 monad 中可用。
在函数式语言中模拟状态的最常见方法是将状态显式传递为函数参数和返回值。例如:
randomGen :: Seed -> (Int, Seed)
这里randomGen 接受一个种子参数并返回一个新种子。每次调用它时,都需要跟踪下一次迭代的种子。这种技术始终可用于状态传递,但很快就会变得乏味。
可能最常见的 Haskell 方法是使用 monad 来封装这种状态传递。我们可以用这个替换randomGen:
-- a Random monad is simply a Seed value as state
type Random a = State Seed a
randomGen2 :: Random Int
randomGen2 = do
seed <- get
let (x,seed') = randomGen seed
put seed'
return x
现在任何需要 PRNG 的函数都可以在 Random monad 中运行以根据需要请求它们。你只需要提供一个初始状态和计算。
runRandomComputation :: Random a -> Seed -> a
runRandomComputation = evalState
(注意有些函数大大缩短了 randomGen2 的定义;我选择了最明确的版本)。
如果您的随机计算还需要访问IO,那么您可以使用State 的monad 转换器版本StateT。
特别值得注意的是ST monad,它本质上提供了一种机制来封装 IO 特定的突变,使其远离 IO 的其余部分。 ST monad 提供 STRefs,它是对数据的可变引用,也是可变数组。使用 ST,可以这样定义:
randomList :: Seed -> [Int]
其中 [Int] 是从您提供的起始种子开始的无限随机数列表(最终会根据您的 PSRG 循环)。
最后是Functional Reactive Programming。可能目前最突出的库是Yampa 和Reactive,但其他库也值得一看。在 FRP 的各种实现中,有几种可变状态的方法;从我对它们的轻微使用来看,它们通常在概念上类似于 QT 或 Gtk+ 中的信号框架(例如,为事件添加侦听器)。
现在,第一个问题。对我来说,最大的优势是可变状态在类型级别与其他代码分离。这意味着除非在类型签名中明确提及,否则代码不会意外修改状态。它还可以很好地控制只读状态与可变状态(Reader monad vs. State monad)。我发现以这种方式构造我的代码非常有用,并且能够仅从类型签名中判断一个函数是否会意外地改变状态是很有用的。
我个人对在 Haskell 中使用可变状态没有任何保留意见。最大的困难是向以前不需要的东西添加状态可能很乏味,但在我用于类似任务的其他语言(C#、Python)中同样的事情会很乏味。