Existential quantification versus explicit types I developed a card game engine for a german three player card game called Skat. The operating code lives in a StateT SkatEnv (WriterT [Trick] IO) Monad.

data SkatEnv = SkatEnv { piles :: Piles
                       , turnColour :: Maybe TurnColour
                       , skatGame :: Game
                       , players :: Players
                       , currentHand :: Hand
                       , skatSinglePlayer :: Hand }
               deriving Show

type Skat = StateT SkatEnv (WriterT [Trick] IO)


To force the three player character on the typelevel, I decided to use a special Players datatype instead of a list, that uses an existentially quantified type PL that can hold any kind of player, that implements the Player typeclass. To make match specific information available to the Player, the chooseCard etc. functions live in a MonadPlayer Monad.

class (Monad m, MonadIO m) =&gt; MonadPlayer m where
    trump :: m Trump
    turnColour :: m (Maybe TurnColour)
    singlePlayer :: m Hand
    game :: m Game

class (Monad m, MonadIO m, MonadPlayer m) =&gt; MonadPlayerOpen m where
    showPiles :: m (Piles)

class Player p where
    team :: p -&gt; Team
    hand :: p -&gt; Hand
    chooseCard :: (HasCard d, HasCard c, MonadPlayer m)
               =&gt; p
               -&gt; [CardS Played]
               -&gt; [CardS Played]
               -&gt; Maybe [d]
               -&gt; [c]
               -&gt; m (Card, p)
    onCardPlayed :: MonadPlayer m
                 =&gt; p
                 -&gt; CardS Played
                 -&gt; m p
    chooseCardOpen :: MonadPlayerOpen m
                   =&gt; p
                   -&gt; m Card

data PL = forall p. (Show p, Player p) =&gt; PL p

data Players = Players PL PL PL
               deriving Show


By using the MonadPlayer interface I can use my Skat monad for calling the Player's functions without exposing unallowed information, e.g. the other players' cards.

instance MonadPlayer Skat where
    trump = getTrump &lt;$&gt; P.game
    turnColour = gets turnColour
    singlePlayer = gets skatSinglePlayer
    game = gets skatGame


I used the PL wrapper, to be able to implement different players in seperate modules, to keep the code clean and extendible, since I have many different players, e.g. an online player that communicates via a socket or a bot, that heuristically decides which card to play.

Another concern for me is the need of the MonadIO restriction for the
MonadPlayer typeclass. This is needed, because my online player for example needs to send messages via a socket, which obviously needs to live in IO, although the bot player could perfectly deal without it. But since I have one common Player interface, I have to make IO available to every player.

Is it better style, not to use the PL wrapper but implement the Players type as

data Players a b c = Players a b c


This would lead to type variables all over the place, especially for everything SkatEnv related.

I left out many implementation details, e.g. many small type definitions. Please let me know, if you need more details about a specific type.
Can you make Player a datatype instead of a typeclass? The datatype can have exactly the same methods but with the first argument elided. Make Player an instance of Show, and voilá, data Players = Players Player Player Player. One implications is that a bot and a human player are now both just values of the same type, Player. Thus the chooseCard, chooseCardOpen and onCardPlayed of a bot can yield a human with a hand and cards on the table mid-game, and vice versa. I'd love to have the freedom to have a bot play the first few rounds for me, yield to me when the game starts going, and play out the last couple of rounds when I feel victory is secure! And as you note, you already opted to give every player type MonadIO, anyway, so these types are not really looser.