Haskell polymorphic functions with records and class types
Asked Answered
D

3

5

this post is the following of this one.

I'm realizing a simple battle system as toy project, the typical system you can find in games like Final Fantasy et simila. I've solved the notorious "Namespace Pollution" problem with a class type + custom instances. For example:

type HitPoints = Integer
type ManaPoints = Integer

data Status = Sleep | Poison | .. --Omitted
data Element = Fire | ... --Omitted

class Targetable a where
    name :: a -> String
    level :: a -> Int
    hp :: a -> HitPoints
    mp :: a -> ManaPoints
    status :: a -> Maybe [Status]

data Monster = Monster{monsterName :: String,
                       monsterLevel :: Int,
                       monsterHp :: HitPoints,
                       monsterMp :: ManaPoints,
                       monsterElemType :: Maybe Element,
                       monsterStatus :: Maybe [Status]} deriving (Eq, Read)

instance Targetable Monster where
    name = monsterName
    level = monsterLevel
    hp = monsterHp
    mp = monsterMp
    status = monsterStatus


data Player = Player{playerName :: String,
                     playerLevel :: Int,
                     playerHp :: HitPoints,
                     playerMp :: ManaPoints,
                     playerStatus :: Maybe [Status]} deriving (Show, Read)

instance Targetable Player where
    name = playerName
    level = playerLevel
    hp = playerHp
    mp = playerMp
    status = playerStatus

Now the problem: I have a spell type, and a spell can deal damage or inflict a status (like Poison, Sleep, Confusion, etc):

--Essentially the result of a spell cast
data SpellEffect = Damage HitPoints ManaPoints
                 | Inflict [Status] deriving (Show)


--Essentially a magic
data Spell = Spell{spellName :: String,
                   spellCost :: Integer,
                   spellElem :: Maybe Element,
                   spellEffect :: SpellEffect} deriving (Show)

--For example
fire   = Spell "Fire"   20 (Just Fire) (Damage 100 0)
frogSong = Spell "Frog Song" 30 Nothing (Inflict [Frog, Sleep])

As suggested in the linked topic, I've created a generic "cast" function like this:

--cast function
cast :: (Targetable t) => Spell -> t -> t
cast s t =
    case spellEffect s of
        Damage hp mana -> t
        Inflict statList -> t

As you can see the return type is t, here showed just for consistency. I want be able to return a new targetable (i.e. a Monster or a Player) with some field value altered (for example a new Monster with less hp, or with a new status). The problem is that i can't just to the following:

--cast function
cast :: (Targetable t) => Spell -> t -> t
cast s t =
    case spellEffect s of
        Damage hp' mana' -> t {hp = hp', mana = mana'}
        Inflict statList -> t {status = statList}

because hp, mana and status "are not valid record selector". The problem is that I don't know a priori if t will be a monster or a player, and I don't want to specify "monsterHp" or "playerHp", I want to write a pretty generic function. I know that Haskell Records are clumsy and not much extensibile...

Any idea?

Bye and happy coding,

Alfredo

Detection answered 17/9, 2011 at 10:41 Comment(2)
Why do players and monsters need to be different types in the first place? They seem to have a lot in common. What is the difference between them?Kurdistan
For now they are quite the same thing, but as design choice I decided to keep them separeted. I can't exclude they will have different field.. just to name one, a monster can have an element (e.g. piros is a fire monster), a player can't :)Detection
S
5

Personally, I think hammar is on the right track with pointing out the similarities between Player and Monster. I agree you don't want to make them the same, but consider this: Take the type class you have here...

class Targetable a where
    name :: a -> String
    level :: a -> Int
    hp :: a -> HitPoints
    mp :: a -> ManaPoints
    status :: a -> Maybe [Status]

...and replace it with a data type:

data Targetable = Targetable { name   :: String
                             , level  :: Int
                             , hp     :: HitPoints
                             , mp     :: ManaPoints
                             , status :: Maybe [Status]
                             } deriving (Eq, Read, Show)

Then factor out the common fields from Player and Monster:

data Monster = Monster { monsterTarget   :: Targetable
                       , monsterElemType :: Maybe Element,
                       } deriving (Eq, Read, Show)

data Player = Player { playerTarget :: Targetable } deriving (Eq, Read, Show)

Depending on what you do with these, it might make more sense to turn it inside-out instead:

data Targetable a = Targetable { target :: a
                               , name   :: String
                               -- &c...
                               }

...and then have Targetable Player and Targetable Monster. The advantage here is that any functions that work with either can take things of type Targetable a--just like functions that would have taken any instance of the Targetable class.

Not only is this approach nearly identical to what you have already, it's also a lot less code, and keeps the types simpler (by not having class constraints everywhere). In fact, the Targetable type above is roughly what GHC creates behind the scenes for the type class.

The biggest downside to this approach is that it makes accessing fields clumsier--either way, some things end up being two layers deep, and extending this approach to more complicated types can nest them deeper still. A lot of what makes this awkward is the fact that field accessors aren't "first class" in the language--you can't pass them around like functions, abstract over them, or anything like that. The most popular solution is to use "lenses", which another answer mentioned already. I've typically used the fclabels package for this, so that's my recommendation.

The factored-out types I suggest, combined with strategic use of lenses, should give you something that's simpler to use than the type class approach, and doesn't pollute the namespace the way having lots of record types does.

Strikebreaker answered 17/9, 2011 at 19:8 Comment(3)
Thanks for the suggestions. I've already took a look to fclabels package, but its apoption seems quite controversial "One hindrance to the adoption of fclabels is that the main package includes the template-haskell plumbing, so the package is not Haskell 98, and it also requires the (fairly non-controversial) TypeOperators extension."Detection
ps. For now I'll try to keep my code as clean as possibile, avoiding fclabels and collaptsing Monster and Player into one common type :)Detection
@Alfredo Di Napoli: I wouldn't worry too much about fclabels. The TypeOperators extension really is no big deal, and the Template Haskell is just used to auto-generate the lenses rather than needing to define them yourself, which means that your actual code won't depend on TH at all. There are other lens packages as well, though, so maybe one of those would suit your preferences better?Strikebreaker
B
3

I can suggest three possible solutions.

1) Your types are very OO-like, but Haskell can also express "sum" types with parameters:

data Unit = UMon Monster | UPlay Player

cast :: Spell -> Unit -> Unit
cast s t =
case spellEffect s of
    Damage hp' mana' -> case t of
                          UMon m -> UMon (m { monsterHp = monsterHp m - hp', monsterMana = undefined})
                          UPluy p -> UPlay (p { playerHp = playerHp p - hp'})
    Inflict statList -> undefined

Thing that are similar in OO-design often become "sum" types with parameters in Haskell.

2) You can do what Carston suggests and add all your methods to type classes.

3) You can change your read-only methods in Targetable to be "lenses" that expose both getting and setting. See the stack overflow discussion. If your type class returned lenses then it would make your spell damage possible to apply.

Brassica answered 17/9, 2011 at 12:39 Comment(2)
Well, I quite like your first solution, it seems shorten then Carsten one. As usual, is a sort of hack/workaround to fill the Record holes :) I also would like to take a look to lenses et simila, despite the fact that I want to avoid a "getters/setters" mechanism that I hate in Java :)Detection
@Alfredo Di Napoli: Getters and setters are bad in Java in large part because they go against the whole concept of good OO design, which is to abstract over behavior and internal implementations. On the other hand, data types here are intended to let you access their internals. Having a lot of getter and setter methods comes from trying to impose the way you'd normally do it in Haskell onto Java. If memory serves me, Scala actually unifies the two directly, with syntactic sugar for different ways of using the type.Strikebreaker
I
1

Why don't you just include functions like

InflicteDamage :: a -> Int -> a
AddStatus :: a -> Status -> a

into your type-class?

Instinct answered 17/9, 2011 at 10:55 Comment(5)
Yes, this is a possibile solution, but forces me to write two almost equals functions in my instances, where the only difference will be in the fields names.. boring and so much boilerplate code :PDetection
then go with the answer of Chris - but this is the old discussion - in this case it's easier but gets hard if you add another type of "Unit".Instinct
Yes, both solutions have pro and cons, I'll leave the question opened for a while, then will go for the answer. Thanks to everyone, I know that it's not your fault but Haskell clumsy records :)Detection
I think it's just that OO-design is clumsy in Haskell as FP desing (with typeclasses) may be very hard or impossible in other languagues. But go on - either way I'm curious on the result of your work ;)Instinct
Just to be explicit, the last thing I want is to emulate OO-design / OO-languages in Haskell, but I can't figure out a better way to encapsulate this kind of behavior with FP (e.g. every spell has it's effect) and I'm clashing always on Records :P Stay tuned, and help me as well :DDetection

© 2022 - 2024 — McMap. All rights reserved.