Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Sum Types vs. Type Classes vs. Records

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
like image 911
theo Avatar asked Mar 23 '21 19:03

theo


People also ask

What is a sum type programming?

In computer science, a sum type, is a data structure used to hold a value that could take on several different, but fixed, types. In practical terms, a Sum Type can be thought of as an enum, with a payload (where that payload is data).

Is maybe a sum type?

A really popular and useful example of a Sum type in the functional world is the Maybe type. In Haskell, Maybe is a monad that wraps a value and helps you make sure that invalid values are not acted upon, thus allowing you to write safer functions.

Is enum a sum type?

Enumerated types are a special case of sum types in which the constructors take no arguments, as exactly one value is defined for each constructor. Values of algebraic types are analyzed with pattern matching, which identifies a value by its constructor or field names and extracts the data it contains.

Does Java have sum types?

The Sealed classes and Record classes provide sum and product types for Java. These algebraic data types are great and they provide a cleaner way to pattern match. The pattern matching provides concise, readable code, and exhaustive type checks by the compiler.


1 Answers

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.

like image 181
Ben Avatar answered Sep 24 '22 02:09

Ben