简体   繁体   中英

On Haskell, what is the linguistic way to represent a card effect for a card game?

I have a simple one-player Card Game:

data Player = Player {
    _hand :: [Card],
    _deck :: [Card],
    _board :: [Card]}
$(makeLenses ''Player)

Some cards have an effect. For example, "Erk" is a card with the following effect:

Flip a coin. If heads, shuffle your deck.

I've implemented it as such:

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)

While this certainly does the job, I find an issue on the forced coupling to the Random library. What if I later on have a card that depends on another monad? Then I'd have to rewrite the definition of every card defined so far (so they have the same type). I'd prefer a way to describe the logic of my game entirely independent from the Random (and any other). Something like that:

erk :: CardAction
erk = do
    coin <- flipCoin
    case coin of
        Head -> shuffleDeck
        Tail -> doNothing

I could, later on, have a runGame function that does the connection.

runGame :: (RandomGen g) => g -> CardAction -> Player -> Player

I'm not sure that would help. What is the correct, linguistic way to deal with this pattern?

This is one of the engineering problems the mtl library was designed to solve. It looks like you're already using it, but don't realize its full potential.

The idea is to make monad transformer stacks easier to work with using typeclasses. A problem with normal monad transformer stacks is that you have to know all of the transformers you're using when you write a function, and changing the stack of transformers changes how lifts work. mtl solves this by defining a typeclass for each transformer it has. This lets you write functions that have a class constraint for each transformer it requires but can work on any stack of transformers that includes at least those.

This means that you can freely write functions with different sets of constraints and then use them with your game monad, as long as you game monad has at least those capabilities.

For example, you could have

erk  :: MonadRandom m => ...
incr :: MonadState GameState m => ...
err  :: MonadError GameError m => ...
lots :: (MonadRandom m, MonadState GameState m) => ...

and define your Game a type to support all of those:

type Game a = forall g. RandT g (StateT GameState (ErrorT GameError IO)) a

You'd be able to use all of these interchangeably within Game , because Game belongs to all of those typeclasses. Moreover, you wouldn't have to change anything except the definition of Game if you wanted to add more capabilities.

There's one important limitation to keep in mind: you can only access one instance of each transformer. This means that you can only have one StateT and one ErrorT in your whole stack. This is why StateT uses a custom GameState type: you can just put all of the different things you may want to store throughout your game into that one type so that you only need one StateT . ( GameError does the same for ErrorT .)

For code like this, you can get away with just using the Game type directly when you define your functions:

flipCoin :: Game a -> Game a -> Game a
flipCoin a b = ...

Since getRandom has a type polymorphic over m itself, it will work with whatever Game happens to be as long as it has at least a RandT (or something equivalent) inside.

So, to answer you question, you can just rely on the existing mtl typeclasses to take care of this. All of the primitive operations like getRandom are polymorphic over their monad, so they will work with whatever stack you end up with in the end. Just wrap all your transformers into a type of your own ( Game ), and you're all set.

This sounds like a good use-case for the operational package. It lets you define a monad as a set of operations and their return types using a GADT and you can then easily build an interpreter function like the runGame function you suggested. For example:

{-# 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...

However, you might also want to consider just describing all your card actions as a simple ADT without do-syntax. Ie

data CardAction
    = CoinFlip CardAction CardAction
    | ShuffleDeck
    | DoNothing

erk :: CardAction
erk = CoinFlip ShuffleDeck DoNothing

You can easily write an interpreter for the ADT and as a bonus you can also eg generate the card's rule text automatically.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM