3
votes

As some form of experimentation, I am trying to define a very general logic for card games in Haskell. To guarantee the maximal level of abstraction, I define the following type class:

class (Monad m) => CardGame m where
    data Deck   m :: *
    data Card   m :: *
    data Player m :: *

    game :: m () -- a description of the game's rules 

    draw :: Deck m -> m (Card m)             -- top card of deck is drawn
    pick :: Deck m -> Player m -> m (Card m) -- player selects card from a deck
    -- so on and so forth

This class is very satisfying to me because it leaves open what cards will be used (e.g. tarot card, French suited, German suited, etc.), who the players are, etc., and what kind of underlying monad the game should be run into.

Now suppose I want to define War-like card games. These games are played between two players, each player receiving one deck, on a French-suited card game but there are many variants of the game itself so I can't specify ahead of time what the exact rules will be (i.e. the instantiation of game) How can I partially specialize the type class above? The compiler gets upset at the following:

data Suit   = Heart | Diamond | Spade | Club
data Value  = Number Int | Jack | Queen | King 
data FrenchSuited = Card Suit Value


class (Game m) => War m where
    -- syntax error in the following line:
    data Card m = FrenchSuited -- any War-like game must be played on French-suited card
    deck1   :: Deck m
    deck2   :: Deck m
    player1 :: Player m
    player2 :: Player m

Do you see a way I can accomplish this?

PS: War-like games can be played with any type of cards really but it's just an example. I am more interested in how one can achieve the type of partial specialization of type classes I am after.

2

2 Answers

5
votes

You can't do that with data families: Card m must be a distinct type to Card m' for distinct m and m'. This is because data families are required to be injective, like regular type constructors.

At best, Card m and Card m' could be both isomorphic to FrenchSuited.

Type families, instead, do not have this injectivity restriction, but because of that they sometimes make type checking more subtle, requiring the user to solve some ambiguities in some way. That being said, you can require your type as follows:

{-# LANGUAGE TypeFamilies, AllowAmbiguousTypes #-}

class (Monad m) => CardGame m where
    type Deck   m :: *
    type Card   m :: *
    type Player m :: *

    game :: m () -- a description of the game's rules 

    draw :: Deck m -> m (Card m)             -- top card of deck is drawn
    pick :: Deck m -> Player m -> m (Card m) -- player selects card from a deck
    -- so on and so forth


data Suit   = Heart | Diamond | Spade | Club
data Value  = Number Int | Jack | Queen | King 
data FrenchSuited = Card Suit Value


class (CardGame m, Card m ~ FrenchSuited) => War m where
    deck1   :: Deck m
    deck2   :: Deck m
    player1 :: Player m
    player2 :: Player m

The Card m ~ FrenchSuited constraint forces type equality in the subclass.

0
votes

The initial question poses the data type classes question. But code already uses type families, data Deck m :: * inside type class requires/is a type family.

To keep things simple, you can use the type classes for Player, Card, Deck for initial prototyping to see how things would go, and it would keep the code simple. And as structure emerges to feel the pressure points, where structure requires more clever abstractions and you can move to the type families as you would see fit. But also this use of type families seems good right upfront.

Also do not forget that type classes/families can be "partially instantiated" (as the question sounds) with:

{-# MINIMAL funA | funB #-}

AKA you can include more/all into the class, but require minimally those definitions.