除了纯函数的概念之外,我对函数式编程知之甚少。在John Carmack 2013年的Quakecon演讲中,他提到了一个常常被问到的与游戏相关的函数式编程问题:如果你没有状态,你如何开枪并对另一个玩家造成伤害? (转述)在提到一个关于事件系统的事情,我不太明白,因为在我看来事件系统仍然需要状态?
如何用纯粹的功能性语言实现这一目标?
答案 0 :(得分:8)
重复我最喜欢的一句话
......接受世界状态,回归新世界,保持纯洁。
这是在讨论Clean,Haskell的堂兄,但它仍然有关系。它的要点是,你是对的,你需要某种状态,但它不一定是可变的。考虑
myFun :: StateOfTheWorld -> a -> (StateOfTheWorld, b)
所以我们不修改状态,我们只是生成一个新状态。这是参考透明的,因为给定相同的世界状态和相同的动作,你会得到同样的回报。
对你来说,你可能会有像
这样的东西 killPlayer :: Game -> Event -> Game
killPlayer g (Kill x) = g { isDead = x : isDead g }
它只是使用记录的功能更新。这有点笨拙,所以我们可能会做类似
的事情 killPlayer :: Game -> Event -> Action
killPlayer (PlayerDamaged x amount) = if playerHealth g x <= amount
then KillPlayer x
else ReduceHealth x amount
所以我们只是回归差异,而不是完整的游戏状态。
这很有效,但很难看。所以我们用do
表示法和Control.Monad.State来解决这个问题。这听起来很可怕,但它正是我们上面所做的,只是更多的语法抽象。事实上,这也是IO
对GHC的影响。我不知道你是否了解过Monads,但State monad通常是激励人心的例子。
最后为了回到游戏,我见过的许多游戏框架都是这样的:成堆的东西听取事件,然后建议对游戏状态进行一些小的增量更改并返回不同的,最后框架本身使得适当的openGL调用或任何实现这些更改的内容。
答案 1 :(得分:2)
状态只是环境中的一组价值观。 Haskell让您明确地对待您的环境,因此我们可以将其称为Env
。我们创造了新的
letThereBeLight :: Env
letThereBeLight = Env { personHealth = 100 }
并修改它们
shootEmUp :: Env -> Env
shootEmUp oldEnv = oldEnv { personHealth = personHealth oldEnv - 30 }
类似Env -> Env
的类型称为Endo Env
,因为您可以端到端地应用它们以对状态进行许多更改。
assassinate = shootEmUp . shootEmUp . shootEmUp . shootEmUp
如果你想做的不仅仅是 修改状态,你需要在状态Endo
旁边对其他值进行排序。您可以开始看Env -> Env
,而不是看起来像Env -> (Env, a)
,其中a
为您的其他数据流建模并从那里构建。这个东西被称为Monad状态,因为有一些非常聪明的方法可以很容易地将这两个信息流一起操作。
答案 2 :(得分:1)
为了支持我的评论,这是一个改编自http://www.haskellforall.com/2013/05/program-imperatively-using-haskell.html的例子:
-- -----------------------------------------------------------------------------
-- * Our homegrown state monad (use @State@ from the MTL package in production).
-- | @State@ is a function (lets call it "state-updater") which "updates" a
-- state @s@ and returns some associated result @r@.
newtype State s r = State { run :: s -> (r, s) }
-- | This state-updater function is a monad.
instance Monad (State s) where
-- | Build a state-updater which returns @x@ and don't change the state.
return x = State $ \st -> (x, st)
-- | From a state-updater @m@ and a function @f@ which returns a state-updater
-- we can build a new (lazy) state-updater by performing update actions of this two
-- state-updaters.
m >>= f = State $ \st -> let (x, st') = run m st in run (f x) st'
-- | Simply swap the state.
put :: s -> State s ()
put st = State $ const ((), st)
-- | Get the current state as a result of this state-updater.
get :: State r r
get = State $ \st -> (st, st)
-- -----------------------------------------------------------------------------
-- * An example.
-- | Player with its health.
newtype Player = Player { _health :: Int } deriving ( Show )
-- | Game of two players.
data Game = Game { _player1 :: !Player, _player2 :: !Player } deriving ( Show )
-- | Starting from weak and strong players.
initialState :: Game
initialState = Game (Player 10) (Player 20)
-- | First player hit second.
hit12 :: State Game ()
hit12 = do
g@(Game _ p2@(Player health)) <- get
put g { _player2 = p2 { _health = health - 1 } }
-- | Second player hit first.
hit21 :: State Game ()
hit21 = do
g@(Game p1@(Player health) _) <- get
put g { _player1 = p1 { _health = health - 1 } }
-- | Test it.
test :: ((), Game)
test = run (do { hit12; hit12; hit12; hit21 }) initialState
--
-- initialState
-- =>
-- Game {_player1 = Player {_health = 10}, _player2 = Player {_health = 20}}
--
-- snd test
-- =>
-- Game {_player1 = Player {_health = 9}, _player2 = Player {_health = 17}}
--
Lenses允许写
hit12 = player2.health -= 1
hit21 = player1.health -= 1
State transformer(无论如何你应该使用)允许将另一个monad(如IO
)与State
混合,但基本上它都是纯粹的并且工作方式如下:
......接受世界的状态并返回一个新的世界 保持纯洁。
引用另一条评论。