I recently started a little hobby project, where I try to implement the trick-taking card-game Skat (for 3 players). To make it possible to have different kinds of players (like AI, network and local) playing together, I designed an interface using a typeclass Player
:
class Monad m => Player p m | p -> m where
playerMessage :: Message answer -> p -> m (Either Error answer,p)
I use a StateT
to wrap up those three players:
type PST a b c m x = StateT (Players a b c) m x
But now, I have to write a big pile of context in each type signature:
dealCards :: (Player a m, Player b m, Player c m, RandomGen g)
=> g -> PST a b c m (SomeOtherState,g)
How can I avoid writing this big context again and again?
The only thing you can observe from the player class is a function of type
playerMessage' :: Message answer -> m (Either Error answer, p)
Hence, you can eliminate the class entirely and use an ordinary data type
data Player m = Player { playerMessage'
:: Message answer -> m (Either Error answer, Player m) }
This is basically my previous answer.
An alternative solution is to move the context into the data type by using GADTs.
data PST a b c m x where
PST :: (Player a m, Player b m, Player c m)
=> StateT (Players a b c) m x -> PST a b c m x
In other words, the constraints become part of the data type.
The best solution is, in my opinion, to scrap the whole thing and redesign it along the lines of the TicTacToe example from my operational package. This design allows you to write each player (human, AI, replay,...) in a specialized monad and later inject everything into a common interpreter.
Update:
When I tried implementing dealCards
I have realised that my solution decreases the type-safety by making the players interchangeable. This way you can easily use one player instead of the other which may be undesirable.
If you don't mind using ExistentialQuantification
, I think it can (and should?) be used here. After all, the dealCards
function should not care or know about a
, b
and c
, right?
{-# LANGUAGE ExistentialQuantification #-}
{-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE FunctionalDependencies #-}
import Control.Monad.State
import System.Random
type Message answer = answer
type Error = String
class Monad m => Player p m | p -> m where
playerMessage :: Message answer -> p -> m (Either Error answer,p)
data SomePlayer m = forall p. Player p m => SomePlayer p
data Players m = Players (SomePlayer m) (SomePlayer m) (SomePlayer m)
type PST m x = StateT (Players m) m x
dealCards :: (RandomGen g, Monad m) => g -> PST m x
dealCards = undefined
I think it should be possible to eliminate the Monad
constraint in a similar way.
Actually, in cases like this I feel like type classes are being overused. Maybe that's a Haskell novice speaking in me, but I'd write this instead:
data Player m = Player { playerMessage :: Message answer -> m (Either Error answer, Player m) }
Clearly the better answer is to have a design that doesn't need all those type parameters in the first place. However, if you really can't get rid of them, and to answer the question as posed, here's a trick I've played:
class (Storable.Storable (X, y), Y y) => Signal y
Now writing '(Signal y) => ...' will imply all the other typeclasses, and prevent implementation details like Storable from getting into every API. However, you have to declare instances for Signal. It's easy because it has no methods, but is probably mostly suitable for when you have few instances but many functions.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With