Haskell和状态

14

Haskell是一种纯函数式编程语言。

我的问题是:使用Haskell解决涉及大量状态的问题(例如GUI编程或游戏编程)有哪些优缺点?

还有一个次要问题:有哪些方法可以用函数式的方式处理状态?

提前感谢您的帮助。


4
在函数式语言中处理状态需要将状态传递给各个函数。单子(monads)可以简化这一过程。 - tylermac
@tylermac 我从来没有能够理解单子,虽然我不能说我很蠢,至少我有计算机科学学士学位,但是单子...你知道好的教程吗? - Andrey
@Andrey,我知道的最好的单子教程是http://blog.sigfpe.com/2006/08/you-could-have-invented-monads-and.html,还可以尝试http://stackoverflow.com/questions/2366/can-anyone-explain-monads。 - John L
learnyouahaskell.com有一个关于单子的教程 - 尽管我还没有看到它,但就我目前的进度而言,一切似乎都写得非常好,易于理解! - bogatyrjov
从传统实现的角度来看,GUI编程和游戏编程只有“大量状态”--问题本质上并没有什么状态。此时,大多数人会对这个说法的明显虚假做出反应。相反,为什么不阅读这篇论文?http://conal.net/papers/icfp97/ - luqui
显示剩余4条评论
5个回答

18

我先回答你的第二个问题。实际上,Haskell(以及其他函数式编程语言)有许多处理可变状态的方法。首先,在IO中,Haskell支持可变状态通过IORefmvar结构体。使用这些会对命令式语言的程序员非常熟悉。还有特殊版本,如STRefTMVar,以及可变数组、指针和各种其他可变数据。最大的缺点是这些通常只在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的定义; 我选择了最明确的版本)
如果您的随机计算还需要访问IO,则使用State的monad transformer版本StateT。
特别说明一下ST monad,它实质上提供了一种机制,将特定于IO的突变封装在与IO其他部分不相关的位置。ST monad提供了STRefs,这是对数据的可变引用,以及可变数组。 使用ST,可以定义类似于以下内容:
randomList :: Seed -> [Int]

其中 [Int] 是一个无限的随机数列表(根据您提供的PSRG,它最终会循环)。

最后,还有函数响应式编程。 目前最突出的库可能是YampaReactive,但其他库也值得一看。 在各种FRP实现中,有几种处理可变状态的方法。 从我轻微的使用经验来看,它们在概念上通常类似于QT或Gtk+中的信号框架(例如添加事件的监听器)。

现在,对于第一个问题。 对我而言,最大的优点是可变状态在类型级别上与其他代码分离。 这意味着除非在类型签名中明确提到,否则代码不能意外修改状态。 它还可以非常好地控制只读状态与可变状态(Reader monad vs. State monad)。 我发现以这种方式结构化我的代码非常有用,并且能够仅从类型签名中判断函数是否可能意外地修改状态非常有用。

就我个人而言,我对在Haskell中使用可变状态没有什么保留意见。 最大的困难在于向先前不需要状态的内容添加状态可能会很繁琐,但是对于我使用类似任务的其他语言(C#,Python)也同样繁琐。


这里的“Seed”是什么?我尝试在文件顶部将其定义为int(type Seed = Int),但出现错误“No instance for (Show (Random Int))”(我最初使用了您的代码理论,但它没有起作用,所以尝试将其全部复制粘贴,然后出现了这个错误)。 - Sam Heather
这更像是伪代码而不是实际实现。Seed是表示PRNG状态相关数据的抽象类型。我使用PNRG,因为它是一类有状态算法的众所周知的示例。如果您需要随机数生成器,我建议使用像mwc-randommersenne-random这样的包。 - John L
谢谢回复 - 我正在努力理解moads而不是使用PRNG,但我认为这是一个非常好的例子。但我完全被卡住了。几个小时前我在SO上问了一个关于这个问题的问题,但现在更加困惑了!http://stackoverflow.com/questions/23595363/simple-haskell-monad-random-number/ - Sam Heather

10

4
使用Haskell解决涉及大量状态的问题,例如GUI编程或游戏编程的优缺点是什么?
优点在于,即使您没有特别利用纯度,Haskell仍然是一种好语言。它具有一流的函数、代数类型与模式匹配、强大的静态类型检查与类型推断、清晰的语法、一流的并发性、STM和无线程纯并行性、良好的编译器、大量的库以及每天都有更多。这些不是像纯度或惰性这样的大型意识形态决定。它们只是好主意。它们是大多数语言都可以拥有的东西,但太多语言没有。

你能告诉我什么是一流的并发性吗? - user1198582

4
一个状态单子模型是用Haskell建模GUI或游戏的最糟糕的方式。我认为第二个最佳选项是在这两种情况下使用并发。然而,最好的选择是由Paul提到的功能反应式编程(FRP)。就个人而言,我倡导箭头化的FRP(AFRP),我认为它最初是作为Yampa实现的,稍后又被分叉为略微更有用的Animas。然而,Yampa很快达到了其极限,因此我编写了一个更强大、更具表现力的库,称为netwire,它也比前两个有一些概念上的改进。在本质上,AFRP是一个功能状态系统。它是功能性的,因为状态不是建模为变量值的变化,而是变异函数。这更加清洁,不需要像状态单子那样的命令式编程。

2

通常情况下,您需要使用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

这段代码如果你不理解单子的话就会比较复杂,但我的经验法则是,找到 State Monad 的作用,了解 Monad Transformers 是什么(无需理解它们如何工作),以及如何使用 StateT 单子。
我可以指向一个 Sokoban 项目,我和其他队友一起完成的,这个项目可能会有用,它使用 ncurses 作为 GUI,但你可以了解逻辑以及我们如何管理游戏状态的想法。 http://github.com/roman/HaskBan 祝你好运。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接