简体   繁体   中英

Haskell polymorphic functions with records and class types

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 (ie 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

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.

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.

Why don't you just include functions like

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

into your type-class?

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM