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 Int
s and be done with it.