Haskell是一种纯函数式编程语言。
我的问题是:使用Haskell解决涉及大量状态的问题(例如GUI编程或游戏编程)有哪些优缺点?
还有一个次要问题:有哪些方法可以用函数式的方式处理状态?
提前感谢您的帮助。
Haskell是一种纯函数式编程语言。
我的问题是:使用Haskell解决涉及大量状态的问题(例如GUI编程或游戏编程)有哪些优缺点?
还有一个次要问题:有哪些方法可以用函数式的方式处理状态?
提前感谢您的帮助。
我先回答你的第二个问题。实际上,Haskell(以及其他函数式编程语言)有许多处理可变状态的方法。首先,在IO中,Haskell支持可变状态通过IORef
和mvar
结构体。使用这些会对命令式语言的程序员非常熟悉。还有特殊版本,如STRef
和TMVar
,以及可变数组、指针和各种其他可变数据。最大的缺点是这些通常只在IO或更专门的monad内部可用。
模拟函数式语言中状态的最常见方法是将状态明确地作为函数参数和返回值传递。例如:
randomGen :: Seed -> (Int, Seed)
这里的randomGen
需要一个种子参数并返回一个新的种子。每次调用它时,您需要跟踪下一次迭代的种子。这种技术始终可用于状态传递,但很快会变得乏味。
可能最常见的Haskell方法是使用单子来封装此状态传递。我们可以使用单子替换 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的定义; 我选择了最明确的版本)randomList :: Seed -> [Int]
其中 [Int] 是一个无限的随机数列表(根据您提供的PSRG,它最终会循环)。
最后,还有函数响应式编程。 目前最突出的库可能是Yampa和Reactive,但其他库也值得一看。 在各种FRP实现中,有几种处理可变状态的方法。 从我轻微的使用经验来看,它们在概念上通常类似于QT或Gtk+中的信号框架(例如添加事件的监听器)。
现在,对于第一个问题。 对我而言,最大的优点是可变状态在类型级别上与其他代码分离。 这意味着除非在类型签名中明确提到,否则代码不能意外修改状态。 它还可以非常好地控制只读状态与可变状态(Reader monad vs. State monad)。 我发现以这种方式结构化我的代码非常有用,并且能够仅从类型签名中判断函数是否可能意外地修改状态非常有用。
就我个人而言,我对在Haskell中使用可变状态没有什么保留意见。 最大的困难在于向先前不需要状态的内容添加状态可能会很繁琐,但是对于我使用类似任务的其他语言(C#,Python)也同样繁琐。
type Seed = Int
),但出现错误“No instance for (Show (Random Int))”(我最初使用了您的代码理论,但它没有起作用,所以尝试将其全部复制粘贴,然后出现了这个错误)。 - Sam HeatherSeed
是表示PRNG状态相关数据的抽象类型。我使用PNRG,因为它是一类有状态算法的众所周知的示例。如果您需要随机数生成器,我建议使用像mwc-random
或mersenne-random
这样的包。 - John L虽然我不怀疑人们会回答“使用状态单子”,但我想指出另一种有用的方法:函数响应式编程(使用Yampa或其他库)。
通常情况下,您需要使用Monad Transformer和StateT以及IO一起使用,这是因为视图(GUI)需要IO才能响应,但是一旦您在newtype
中定义了Monad Transformer,您希望仅使用MonadState
接口的游戏逻辑签名,这样您仍然可以享受非IO性更改的好处。下面的代码解释了我的意思:
{-# LANGUAGE GeneralizedNewtypeDeriving #-}
import Control.Monad.State
data GameState = GameState { ... } deriving (Show)
newtype GameMonad a = GameMonad (StateT GameState IO a)
deriving (Monad, MonadState GameState, MonadIO)
-- This way, now you have a monad with the state of the info
-- what would you like now is being able to modify the state, without actually
-- having IO capabilites.
movePlayerOnState :: (MonadState GameState) m => Position -> m ()
-- In this function we get the state out of the state monad, and then we modify
-- with a pure function, then put the result back again
-- Other times you would like to have the GUI API available, requiring the IO monad
-- for that
renderGameFromState :: MonadIO m => GameState -> m ()
-- in this method you would use liftIO method to call the GUI API