在Haskell上,用什么语言方式来表示纸牌游戏的牌效应?

时间:2015-03-09 06:48:57

标签: haskell design-patterns functional-programming dsl

我有一个简单的单人纸牌游戏:

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

我不确定这会有所帮助。处理这种模式的正确语言方法是什么?

2 个答案:

答案 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 。 (GameErrorErrorT执行相同操作。)

对于这样的代码,您可以在定义函数时直接使用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编写翻译,作为奖励,您也可以例如自动生成卡片的规则文本。