10
votes

Despite the title I'm not going to ask about a mere translation between OO world and Haskell, but I can't figure out a better title. This discussion is similar, but not equal, to this one.

I've started a toy project just to expand my limited knowledge of Haskell while reading "Learn You a Haskell for a Great Good", and I've decided to implement a very basic "Elemental Type System", which is a subset of a typical battle system in games like Final Fantasy et simila. I'm skipping most of the details, but this is in a nutshell my problem:

I want to model a spell, a magic you can cast on the player or on a monster. In the OO world you usually go for a "Castable" interface with a method "onCast(Player)", a "Spell" class so you can define thing like this

Spell myNewSpell = Spell("Fire", 100, 20);
myNewSpell.onCast(Player p); //models the behaviour for the Fire spell

In Haskell I thought this in terms of Types and Classes (I know that Classes in Haskell are a different concept!). I've encountered some difficulties, because my first attempt was to create this:

--A type synonim, a tuple (HP,MP)
type CastResult = (Integer,Integer)


--A castable spell can either deal damage (or restore) or
--inflict a status
class Castable s where
  onCast :: s -> Either (Maybe Status) CastResult


data Spell = Spell{spellName :: String,
                   spellCost :: Integer,
                   spellHpDmg :: Integer,
                   spellMpDmg :: Integer,
                   spellElem :: Maybe Element} deriving (Eq,Show,Read)

Now suppose I create some spell using the Record Syntax

bio = Spell{spellName = "Bio", ...etc..}

I would like be able to do something like this

instance Castable bio where
  onCast bio = Left (Just Poison)

There are many problems here:

  1. I can't do "Castable bio" since bio must be a concrete type, not a value of the Type (It should be Castable Spell)

  2. bio isn't in scope, inside the instance block is seen just as a value to pattern match against

Overall, I feel this choice of design is pretty poor, but I'm still learning and I don't grasp such advanced topics like Functors, just to name one.

In a nutshell, which is the idiomatic way to dealing with situation like this? I mean situation which requires "one definition, multiple implementation for multiple instances", just to use the OO terminology.

3
Rule of thumb: Model OO classes as records of functions, ignore subclasses that just override the same behavior, use functions that create something of the appropriate type in place of constructors for the simple subclasses, and forget about type classes for now. Type classes are how you model abstract interfaces and function overloading, not OO classes. - C. A. McCann
"Overall, I feel this choice of design is pretty poor" - give it some time; coming from OO to Haskell can be difficult because of the many things you must unlearn in order to feel comfortable with the pure FP approach. - Dan Burton

3 Answers

14
votes

Type classes are useful when you're dealing with different types. In this case, however, it seems to me like you're dealing with separate instances. In such a case, it's probably simplest to have the cast function be just another record field.

data Spell = Spell{spellName :: String,
                   ...
                   onCast :: Either (Maybe Status) CastResult }
    deriving (Eq,Show,Read)

bio = Spell { spellName = "Bio", onCast = Left (Just Poison), ... } 

Or you could do something that models your requirements more explicitly, using domain-specific types rather than generic ones like Either.

type ManaPoints = Integer
type HitPoints  = Integer

data Spell = Spell { spellName :: String,
                     spellCost :: ManaPoints,
                     spellElem :: Maybe Element,
                     spellEffect :: Effect }

data Effect = Damage  HitPoints ManaPoints
            | Inflict Status

cast :: Spell -> Player -> Player
cast spell player =
    case spellEffect spell of
        Damage hp mana = ...
        Inflict status = ...

bio  = Spell { spellName = "Bio", spellEffect = Inflict Poison, ... }
fire = Spell { spellName = "Fire", spellEffect = Damage 100 0, ... }
3
votes
data Spell = Spell{ spellName :: String
                  , spellCost :: Integer
                  , spellHpDmg :: Integer
                  , spellMpDmg :: Integer
                  , spellElem :: Maybe Element
                  , spellStatus :: Maybe Status
                  }
                  deriving (Eq,Show,Read)

class Castable s where
    onCast :: s -> (CastResult, Maybe Status)

instance Castable Spell where
    onCast s = ((spellHpDmg s, spellMgDmg s), spellStatus s)

This would probably do the trick here, not sure whether the class is useful in this case though. Is something else than a Spell castable? Something like a Skill, or an Item?

3
votes

If I understand you correctly, I think you should make onCast an additional record field of Spell, then you can write:

bio = Spell{spellName = "Bio", ...etc.., onCast = Left (Just Poison)}

You won't be able to do deriving (Eq,Show,Read) anymore though, as Spell now contains a function type. You'll have to write those instances manually. Edit: actually onCast isn't a function type, so ignore this.