Question

Given class X and Y, what's the most idiomatic approach to creating instances of each other's class? e.g. -

instance (X a) => Y a where ...
instance (Y a) => X a where ...

I'd like to avoid extensions. Also, I am aware that this could cause some nasty infinite recursion, so I'm open to a completely different approach to accomplish the same thing and stay relatively DRY. Below gives some context as to the exact problem I am having -

data Dealer = Dealer Hand
data Player = Player Hand Cash

class HasPoints a where
    getPoints :: a -> Int

class (HasPoints a) => CardPlayer a where
    getHand :: a -> Hand

    viewHand :: a -> TurnIsComplete -> Hand

    hasBlackjack :: a -> Bool
    hasBlackjack player = getPoints player == 21 &&
                          (length . getCards . getHand) player == 2

    busts :: a -> Bool
    busts player = getPoints player > 21

I'd like to do this -

instance (CardPlayer a) => HasPoints a where
    getPoints = getPoints . getHand

But it seems I must do this -

instance HasPoints Dealer where
    getPoints = getPoints . getHand

instance HasPoints Player where
    getPoints = getPoints . getHand

EDIT

Seems my favorite approach is to keep the HasPoints typeclass and implement CardPlayer as data instead.

data CardPlayer = Dealer Hand | Player Hand Cash

instance HasPoints CardPlayer where
    getPoints = getPoints . getHand

getCash :: CardPlayer -> Maybe Cash
getHand :: CardPlayer -> Hand
viewHand :: CardPlayer -> TurnIsComplete -> Hand
hasBlackjack :: CardPlayer -> Bool
busts :: CardPlayer -> Bool

-- I wanted HasPoints to be polymorphic
-- so it could handle Card, Hand, and CardPlayer

instance HasPoints Hand where
    getPoints Hand { getCards = [] } = 0

    getPoints hand = if base > 21 && numAces > 0
                     then maximum $ filter (<=21) possibleScores
                     else base
      where base = sum $ map getPoints $ getCards hand
            numAces = length $ filter ((Ace==) . rank) $ getCards hand
            possibleScores = map ((base-) . (*10)) [1..numAces]

instance HasPoints Card where
    -- You get the point
Was it helpful?

Solution

Given class X and Y, what's the most idiomatic approach to creating instances of each other's class?

The most idiomatic approach, given your example code, is to not use type classes in the first place when they're not doing anything useful. Consider the types of the class functions:

class HasPoints a where
    getPoints :: a -> Int

class (HasPoints a) => CardPlayer a where
    getHand :: a -> Hand
    viewHand :: a -> TurnIsComplete -> Hand
    hasBlackjack :: a -> Bool
    busts :: a -> Bool

What do they have in common? They all take exactly one value of the class parameter type as their first argument, so given such a value we can apply each function to it and get all the same information without needing to bother with a class constraint.

So if you want a nice, idiomatic DRY approach, consider this:

data CardPlayer a = CardPlayer
    { playerPoints :: Int 
    , hand :: Hand
    , viewHand :: TurnIsComplete -> Hand
    , hasBlackjack :: Bool
    , busts :: Bool
    , player :: a
    }

data Dealer = Dealer
data Player = Player Cash

In this version, the types CardPlayer Player and CardPlayer Dealer are equivalent to the Player and Dealer types you had. The player record field here is used to get the data specialized to the kind of player, and functions that would have been polymorphic with a class constraint in yours can simply operate on values of type CardPlayer a.

Though perhaps it would make more sense for hasBlackjack and busts to be regular functions (like your default implementations), unless you really need to model players who are immune to the standard rules of Blackjack.

From this version, you can now define a HasPoints class alone if you have very different types that should be instances of it, though I'm skeptical of the utility of that, or you can apply the same transformation to get another layer:

data HasPoints a = HasPoints
    { points :: Int
    , pointOwner :: a
    }

However, this approach quickly becomes unwieldy the further you nest specializations like this.

I would suggest droping HasPoints entirely. It only has one function, which just extracts an Int, so any code that handles HasPoints instances generically might as well just use Ints and be done with it.

OTHER TIPS

In general, it's impossible to declare all instances of a class to also be instances of another class without making type checking undecidable. So your proposed definition will only work with UndecidableInstances enabled:

{-# LANGUAGE FlexibleInstances, UndecidableInstances #-}

instance (CardPlayer a) => HasPoints a where
    getPoints = getPoints . getHand

Although it's possible to go that route, I'd suggest refactoring the code as follows instead:

data Hand = ...

handPoints :: Hand -> Int
handPoints = ...

data Dealer = Dealer Hand
data Player = Player Hand Cash

class CardPlayer a where
  getHand :: a -> Hand
  ...

instance CardPlayer Dealer where ...
instance CardPlayer Player where ...

playerPoints :: (CardPlayer a) => a -> Int
playerPoints = handPoints . getHand
Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top