How to model "subtypes"?

Considering an rpg game, I want to model the concept of “player”, I start writing:

data Player
  = Player
    { hp :: Int
    , position :: Position
    }

data Position
  = Position Int Int

So far so good, now I want to add “goods”, players can carry an goods.
But there are many kinds of “goods”, such as iron swords, such as hematinic, so I wrote:

data ScrapIron
  = ScrapIron { name :: String, addAtk :: Int }

data Hematinic
  = Hematinic { name :: String, addHp :: Int }

class Goods a

instance Goods ScrapIron
instance Goods Hematinic

I wrote a type class and made both types implement the type class, meaning “iron swords and hematinic are both goods”.

This way I can write a function where the player picks up goods:

pickup :: forall goods. Goods goods => Player -> goods -> Player

Although “goods” is generic type here, it is constrained to the “Goods” type class.

But the problem is, I can’t write:

data Player
  = Player
    { hp :: Int
    , position :: Position
    , goods :: Goods
    }

Because “Goods” is a type class and not a type.

So I write:

data Player goods
  = Player
    { hp :: Int
    , position :: Position
    , goods :: goods
    }

Although this works, the program becomes very complicated, and all occurrences of the “Player” type need to pass in a generic parameter, and also qualify it as “Goods”.

To make matters worse, in the future I will add more types and this generic parameter list will get very long.

So, what is the correct way to do it?

I think the motivation for inventing type classes is to describe an “extersion” of a concept, So this way of writing is very natural:

class Goods a

instance Goods ScrapIron
instance Goods Hematinic

Am I understanding it wrong?

If this is wrong, how are type classes commonly used in modeling?

Now I changed it to this:

data Weapon
  = ScrapIron { addAtk :: Int }

data Agentia
  = Hematinic { addHp :: Int }

data Goods
  = Weapon_gen Weapon | Agentia_gen Agentia

data Player
  = Player
    { hp :: Int
    , position :: Position
    , goods :: Goods
    }

class Pickup a where
  pickup :: Player -> a -> Player

instance Pickup Agentia where
  pickup (Player { hp, position }) goods = Player { hp, position, goods: Agentia_gen goods }
instance Pickup Weapon where
  pickup (Player { hp, position }) goods = Player { hp, position, goods: Weapon_gen goods }

which looks good, but is it against some “best practice” or something?
Is there any better way?

Thank for your answer.

1 Like

Hi,

please view this as what it is: My opinion (instead of best-practice or the truth or something like this).

Personally I’d go with just your data Goods = ... type and add from there.
This way you can never miss any place if you want to extent it.

I don’t think type-classes without methods (many argue without rules/laws) are the way to go here.

Sure you want some kind of list that can take on different types but without some common interface it’s not really useful to have (you cannot dispatch/match on the type of such an item at runtime without further heavy hitters like some generics …).


EDIT:

In case you really want to play with this idea (heterogeneous lists, existential types, …): You can encode this with PureScripts type-system (although even a bit more nasty then what you’d have in Haskell - or so I think - maybe there is a cleaner way but I could not hack it together right now):

class Goods a where
  what :: a -> String

instance Goods ScrapIron where
  what _ = "Scrap"

instance Goods Hematinic where
  what _ = "Hemantinic"

data GItem = GItem (forall r. (forall g. Goods g => g -> r) -> r)

mkG :: forall g. Goods g => g -> GItem
mkG g = GItem (\r -> r g)

unG :: forall r. (forall g. Goods g => g -> r) -> GItem -> r
unG x (GItem g) = g x

addGoods :: forall g. Goods g => g -> Array GItem -> Array GItem
addGoods g gs = mkG g : gs

showGoods :: Array GItem -> Array String
showGoods = map (unG what)

someGoods :: Array GItem
someGoods = addGoods (ScrapIron { addAtk: 1, name: "scrap" }) $ addGoods (Hematinic { addHp: 0, name: "hem" }) []

test :: Array String
test = showGoods someGoods
-- this yields ["Scrap","Hemantinic"]

Notice that you need some function for your type-class so that you can operate on the universal quantification


An alternative could be using extensible records but I still think the “easy” ADT variant is the way to go :wink:

2 Likes

I played around with this concept awhile back, because I think PureScript makes it more difficult than most languages to model this sort of thing.

That would let me use variants and PureScript’s record system to let me build OO-style “inheritance”, where e.g., every Goods could have an “inherited” name :: String without pattern matching, but then you could match at runtime for a ScrapIron to get the atk value, or a Hematinic to get the hp value.
But if you do take a look at that experiment, please note all the caveats at the bottom, because this is certainly not idiomatic PureScript.

1 Like

I agree with @CarstenKoenig. Defining a separate ADT to model your “universe” and casing on it is really how the language wants you to solve this problem. If you want extensibility, then something like purescript-variant can help by making the data type more ad-hoc if you find yourself needing to define variations of the same thing with more or fewer constructors. In practice, I think it’s pretty rare to do that except in very specific patterns like modelling exceptions.

The feature you want for your exact usecase (boxing a type with a dictionary and making it abstract) is called existential quantification. The examples given by @CarstenKoenig encodes existential quantification with rank-2 universal quantification. This is something we want to add to the language at some point, but no one has stepped up to implement it yet.

4 Likes

I found a lot of information along the keywords, and also studied the code. Now I understand the methods of “Existential type” and “rank2type” to realize existence type, I have learned a lot, thank you.

This seems to be the “extensible records” method that @CarstenKoenig mentioned?

looks like a sleight of hand, but also fun.

If so, I have another question.
How do people usually use type classes? Or how do programming languages expect me to use type classes?
Just use it for functional polymorphism?

To a first approximation, yes, type classes were designed to enable ad-hoc polymorphism, as distinct from parametric polymorphism (just using a forall a. quantifier) or object-oriented polymorphism. Wadler and Blott said so!

There are certainly cases out there of type classes being used for other purposes, of course. They’re a versatile tool. I wouldn’t advise a beginner to learn about type classes from such cases, though.

2 Likes

Oh! thank! I get it.