Which of the following three approaches would be the most idiomatic to implement some kind of rarity for items in an RPG?
The sum type approach "feels" like it's the right one since rarities seem like a "closed set" (or immutable? idk if that's the right word). But if this were defined in a library then I would not be able to add any more rarities which seems odd.
data Rarity = Rarity { symbol :: String, value :: Int }
common = Rarity "C" 1
rare = Rarity "R" 2
legendary = Rarity "L" 3
data Rarity = Common | Rare | Legendary
symbol :: Rarity -> String
symbol Common = "C"
symbol Rare = "R"
symbol Legendary = "L"
value :: Rarity -> Int
value Common = 1
value Rare = 2
value Legendary = 3
class Rarity r where
symbol :: r -> String
value :: r -> Int
data Common = Common
instance Rarity Common where
symbol _ = "C"
value _ = 1
data Rare = Rare
instance Rarity Rare where
symbol _ = "R"
value _ = 2
data Legendary = Legendary
instance Rarity Legendary where
symbol _ = "L"
value _ = 3
The type class approach you have shown is not very ergonomic in practice.
Presumably you want to have items with a rarity attribute. But what type should the rarity field be? Common
, Rare
, and Legendary
are all separate types, so you can't just have data Item = Item { ..., rarity :: Rarity }
(Not to mention the supposed additional rarity levels added by the client program, if this is in a library).
You can have data Item r = Item { ..., rarity :: r }
, but now the type of a list (or any other generic collection) of items has to specify what single rarity level all the items in it have (e.g. [Item Common]
). That isn't how you want to use rarities in practice (since e.g. a player's inventory can contain items of different rarities!).
You can work around that by using an existential type:
{-# LANGUAGE GADTs #-}
data Item
where Item :: Rarity r =>
{ ...
, rarity :: r
}
-> Item
But now this is basically isomorphic to your first proposal (with data Rarity = Rarity { symbol :: String, value :: Int }
), only way more complicated to use. Since you can't do anything with a value of an unknown type that is constrained to be in the Rarity
type class other than call Rarity
methods, and all those do is get you a String
and an Int
, you might has well have just used a record of a String
and an Int
to begin with, and saved all the extensions and boilerplate.
So now we're down to the first two versions: record or sum type.
A potential advantage of the record version is that you (or any client code) can come up with arbitrary new rarity levels. A big potential disadvantage is that you (or any client code) can come up with arbitrary new rarity levels, without any guarantee of consistency with rarity levels used in other items.
Whether that's a feature or a problem really depends on how you intend to process the String
and Int
that make up a rarity level. If you can truly write engine code in your RPG library that handles those completely agnosticaly of any properties between them, then the record is the right way to go. But my instinct is for RPG rarity levels, you won't do that. You'll rely on an ordering, you'll rely on a given String
rarity code corresponding to the same Int
value each time you see it, etc etc.
I would go for the static sum type when writing a individual RPG. It just fits my conception of rarity levels better (as you say, within one game it normally should be a closed set, not arbitrarily extensible).
For writing an RPG library, I would use a type class, but differently than you used it.
class Rarity r where
symbol :: r -> String
value :: r -> Int
data GameOneRarity = Common | Rare | Legendary
instance Rarity GameOneRarity
where symbol Common = "C"
symbol Rare = "R"
symbol Legendary = "L"
value Common = 1
value Rare = 2
value Legendary = 3
data GameTwoRarity = Boring | Cool | Awesome
instance Rarity GameTwoRarity
where symbol Boring = "B"
symbol Cool = "C"
symbol Awesome = "A"
value Boring = 100
value Cool = 1000
value Awesome = 10000000
I don't have a separate type for each new rarity level, I have a separate type for each rarity scheme. The idea would be for each game to define its own rarity type. An individual set of rarities is not extensible, but the library code can handle arbitrary schemes with whatever rarity levels the game designer wants (so long as they can implement symbol
and value
for them).
The inventory system I write generically in the RPG library can use types like [Item r]
to ensure that all of the items it considers together use the same rarity system (avoiding the possibility of having to answer non-sensible questions like whether a Legendary item from one game is more or less valuable than a Cool item from a different game) but each item can have its own rarity level within that system.