我有一个简单的单人纸牌游戏:
data Player = Player {
_hand :: [Card],
_deck :: [Card],
_board :: [Card]}
$(makeLenses ''Player)
有些卡片有效。例如,“Erk”是具有以下效果的卡片:
Flip a coin. If heads, shuffle your deck.
我已经实现了它:
shuffleDeck :: (MonadRandom m, Functor m) => Player -> m Player
shuffleDeck = deck shuffleM
randomCoin :: (MonadRandom m) => m Coin
randomCoin = getRandom
flipCoin :: (MonadRandom m) => m a -> m a -> m a
flipCoin head tail = randomCoin >>= branch where
branch Head = head
branch Tail = tail
-- Flip a coin. If heads, shuffle your deck.
erk :: (MonadRandom m, Functor m) => Player -> m Player
erk player = flipCoin (deck shuffleM player) (return player)
虽然这确实起到了作用,但我发现有关强制耦合到Random
库的问题。如果我以后有一张取决于另一个monad的卡怎么办?然后我必须重写到目前为止定义的每张卡的定义(所以它们具有相同的类型)。我更喜欢用一种方式来描述我的游戏逻辑完全独立于Random(和任何其他)。这样的事情:
erk :: CardAction
erk = do
coin <- flipCoin
case coin of
Head -> shuffleDeck
Tail -> doNothing
稍后我可以使用runGame
函数来进行连接。
runGame :: (RandomGen g) => g -> CardAction -> Player -> Player
我不确定这会有所帮助。处理这种模式的正确语言方法是什么?
答案 0 :(得分:5)
这是mtl
库旨在解决的工程问题之一。看起来你已经在使用它,但没有意识到它的全部潜力。
我们的想法是使monad变换器堆栈更容易使用类型类。普通monad变换器堆栈的一个问题是,在编写函数时,您必须知道所有变换器,并且更改变换器堆栈会改变升降机的工作方式。 mtl
通过为每个变换器定义一个类型类来解决这个问题。这使您可以编写对所需的每个变换器具有类约束的函数,但可以在任何变换器堆栈上运行,至少包含那些变换器。
这意味着您可以使用不同的约束条件自由编写函数,然后将它们与游戏monad一起使用,只要游戏monad 至少这些功能。
例如,您可以
erk :: MonadRandom m => ...
incr :: MonadState GameState m => ...
err :: MonadError GameError m => ...
lots :: (MonadRandom m, MonadState GameState m) => ...
并定义您的Game a
类型以支持所有这些:
type Game a = forall g. RandT g (StateT GameState (ErrorT GameError IO)) a
您可以在Game
内互换使用所有这些,因为Game
属于所有这些类型类。此外,如果您想添加更多功能,则无需更改除Game
定义之外的任何内容。
要记住一个重要的限制:您只能访问每个变换器的一个实例。这意味着您的整个堆栈中只能有一个StateT
和一个ErrorT
。这就是StateT
使用自定义GameState
类型的原因:您可以将整个游戏中可能要存储的所有不同内容放入该类型中,这样您只需要一个StateT
。 (GameError
对ErrorT
执行相同操作。)
对于这样的代码,您可以在定义函数时直接使用Game
类型:
flipCoin :: Game a -> Game a -> Game a
flipCoin a b = ...
由于getRandom
的类型多态性超过m
本身,只要至少一个Game
,它就可以使用RandT
。内部{1}}(或类似的东西)。
所以,为了回答你的问题,你可以依靠现有的mtl
类型类来处理这个问题。像getRandom
这样的所有原始操作都是monad的多态,所以它们最终会与你最终的堆栈一起工作。只需将所有变换器包装成您自己的类型(Game
),就可以了。
答案 1 :(得分:3)
这听起来像是operational
包的一个很好的用例。它允许您使用GADT将monad定义为一组操作及其返回类型,然后您可以轻松地构建一个解释器函数,如您建议的runGame
函数。例如:
{-# LANGUAGE GADTs #-}
import Control.Monad.Operational
import System.Random
data Player = Player {
_hand :: [Card],
_deck :: [Card],
_board :: [Card]}
data Coin = Head | Tail
data CardOp a where
FlipCoin :: CardOp Coin
ShuffleDeck :: CardOp ()
type CardAction = Program CardOp
flipCoin :: CardAction Coin
flipCoin = singleton FlipCoin
shuffleDeck :: CardAction ()
shuffleDeck = singleton ShuffleDeck
erk :: CardAction ()
erk = do
coin <- flipCoin
case coin of
Head -> shuffleDeck
Tail -> return ()
runGame :: RandomGen g => g -> CardAction a -> Player -> Player
runGame = step where
step g action player = case view action of
Return _ -> player
FlipCoin :>>= continue ->
let (heads, g') = random g
coin = if heads then Head else Tail
in step g' (continue coin) player
...etc...
但是,您可能还想考虑将所有卡片操作描述为一个简单的ADT而不使用do-syntax。即。
data CardAction
= CoinFlip CardAction CardAction
| ShuffleDeck
| DoNothing
erk :: CardAction
erk = CoinFlip ShuffleDeck DoNothing
您可以轻松为ADT编写翻译,作为奖励,您也可以例如自动生成卡片的规则文本。