OO-Like interface implementation in Haskell
Asked Answered
B

3

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.

Bister answered 15/9, 2011 at 12:7 Comment(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.Carroty
"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.Robbierobbin
P
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, ... }
Penetrate answered 15/9, 2011 at 12:59 Comment(4)
Yes, this is probably the best reasonably direct translation.Carroty
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.Please
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 :)Bister
@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.Carroty
B
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?

Bongbongo answered 15/9, 2011 at 12:37 Comment(4)
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 ^^Bister
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.Godunov
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?Bister
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.Bongbongo
H
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.

Hemlock answered 15/9, 2011 at 12:58 Comment(5)
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? :)Bister
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.Hemlock
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.Hemlock
@Sjoerd Make data SpellType a top-level answer (or part of this answer) and I'll vote for it.Vespid
@Daniel Wagner I think hammars answer is even better, having a type value is somewhat indirect.Hemlock

© 2022 - 2024 — McMap. All rights reserved.