10

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.

Jason Aller
  • 3,541
  • 28
  • 38
  • 38
Alfredo Di Napoli
  • 2,281
  • 3
  • 22
  • 28
  • 2
    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 Sep 15 '11 at 13:45
  • 1
    "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 Sep 15 '11 at 16:33

3 Answers3

14

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, ... }
hammar
  • 138,522
  • 17
  • 304
  • 385
  • Yes, this is probably the best reasonably direct translation. – C. A. McCann Sep 15 '11 at 13:47
  • 3
    I really like the second approach. It forgets everything about OO, and just models the problem domain. It seems much more natural to do it that way. – Carl Sep 15 '11 at 17:30
  • Thank you very much, to you and to everyone. Now I have a set of viable options and a bunch of code to think about :) – Alfredo Di Napoli Sep 15 '11 at 17:31
  • 1
    @Carl: It's good to recognize when one approach or another is best. A problem domain suited to decomposable, modular descriptions like this is much more natural using algebraic data types. OO purists generally consider having lots of `getFoo()`, `setFoo()`, &c. methods to be bad design, and rightly so; that's a sign of a class that wants to be an algebraic data type. OO is best for black-box abstractions over behaviors, not inert data. – C. A. McCann Sep 15 '11 at 20:57
3
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?

Ptival
  • 9,167
  • 36
  • 53
  • Thanks for the reply. I figured out your same solution but actually I want be able to write different onCast function based on the spell. For example onCast Fire1 = Fire spell behaviour, onCast Bio1 = Bio spell behaviour, but now that you let me think about it, mine can be a wrong design decision :) ps. I prefer Either rather then a tuple as result ^^ – Alfredo Di Napoli Sep 15 '11 at 12:45
  • The really annoying thing on Haskell is having to workaround not being able to give the same name to record properties in diferent objecs... On topic: A record of functions does the basic job of dynamic dispatching most of the time. However, you might need some extra tricks to make things like "self" and inheritance work if you need those. – hugomg Sep 15 '11 at 12:47
  • Well, I wanna program in functional style, not implementing OO things inside Haskell, so I hope to not need those things :P For the namespace pollution, yes it's the only very annoying thing I've encountered right now, but I'm solving it just with the more verbose typeSuffixNameRecord, just as "spellName" and not only "name". @Ptival: maybe an item can be castable or not, I haven't decided yet, but how can you implement onCast without a class? – Alfredo Di Napoli Sep 15 '11 at 12:56
  • I think at the moment, you need to make a few design decisions on what will be your types, and what will be their shared behaviors. If you think you might need a common interface to do something with different data types, try to express this with a class. However, if the common thing is rather for different elements of a datatype, then make it part of the record. I guess it's kinda hard to anticipate your future needs though, but maybe, think of classes as your OO-interfaces, and of your datatypes as your OO-classes, you should be able to get an intuition for a decent first layout. – Ptival Sep 15 '11 at 13:46
3

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.

Sjoerd Visscher
  • 11,840
  • 2
  • 47
  • 59
  • Actually I'm interested about the possibility to insert a function as a field of my record. Doing so, I can model even the most complex behaviour of my spell, so this can be a solution, but what about the "no-more-deriving" story? :) – Alfredo Di Napoli Sep 15 '11 at 13:08
  • It depends what you need `Eq`, `Show` and `Read` for. It is relatively easy to write you own instances that ignore the functions, but that might not be what you need. – Sjoerd Visscher Sep 15 '11 at 13:17
  • Perhaps a solution is to have a spellType field with type: `data SpellType = Bio | ...` and then do `doCast s = case spellType s of Bio -> Left (Just Poison)` etc. – Sjoerd Visscher Sep 15 '11 at 13:19
  • @Sjoerd Make `data SpellType` a top-level answer (or part of this answer) and I'll vote for it. – Daniel Wagner Sep 15 '11 at 14:26
  • @Daniel Wagner I think hammars answer is even better, having a type value is somewhat indirect. – Sjoerd Visscher Sep 15 '11 at 14:55