At Haskell, what is the linguistic way of presenting a card effect for a card game?

I have a simple single-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 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) => ma -> ma -> ma 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) 

Although this certainly does the job, I find the issue with forcing a link to the Random library. What if I later have a card that depends on another monad? Then I will have to rewrite the definition of each card defined so far (so that they are of the same type). I would prefer a way to describe the logic of my game, completely independent of Random (and any other). Something like that:

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

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

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

I am not sure if this will help. What is the correct, linguistic way to deal with this pattern?

+5
source share
2 answers

This is one of the technical problems with which the mtl library was developed. It looks like you are already using it, but you are not aware of its full potential.

The idea is to simplify the operation of transformer stacks using types. The problem with regular monad transformer stacks is that you need to know all the transformers you use when you write a function, and changing the transformer stack changes how elevators work. mtl solves this by defining a class for each transformer that it has. This allows you to write functions that have a class restriction for each transformer required, but can work with any transformer stack that includes at least those.

This means that you can freely write functions with various sets of restrictions, and then use them in your game monad, if your game monad has at least these features.

For example, you may have

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

and define a Game a type to support all of these:

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

You can use all of these interchangeable elements within the Game because the Game belongs to all these classes. Moreover, you do not need to change anything except the definition of Game if you want to add additional features.

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

For such code, you can simply use the Game type directly when defining your functions:

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

Since getRandom is polymorphic over m , it will work with any Game if it has at least RandT (or something similar) inside.

So, to answer your question, you can simply rely on existing mtl classes to take care of this. All primitive operations, such as getRandom , are polymorphic compared to their monad, so they will work with any stack at the end of which you finish. Just wrap all your transformers in your own ( Game ) and everything will be installed.

+5
source

This seems like a good use case for operational . It allows you to define a monad as a set of operations and their return types using GADT, and then you can easily build an interpreter function, such as the runGame function that you proposed. For instance:

 {-# 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 consider simply describing all the actions on your map as a simple ADT without do-syntax. I.e.

 data CardAction = CoinFlip CardAction CardAction | ShuffleDeck | DoNothing erk :: CardAction erk = CoinFlip ShuffleDeck DoNothing 

You can easily write an interpreter for ADT, and as a bonus, you can also, for example, automatically generate the text of a map rule.

+3
source

Source: https://habr.com/ru/post/1214955/


All Articles