具有记录和类类型的 Haskell 多态函数

发布于 2024-12-05 05:32:53 字数 2731 浏览 0 评论 0原文

这篇文章是这篇文章的后续内容。

我正在实现一场简单的战斗系统作为玩具项目,您可以在《最终幻想》等游戏中找到典型的系统。我已经用类类型+自定义实例解决了臭名昭著的“命名空间污染”问题。例如:

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

现在的问题是:我有一个法术类型,并且一个法术可以造成伤害或造成某种状态(如中毒、睡眠、混乱等):

--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])

正如链接主题中所建议的,我创建了一个通用的“演员”函数如下:

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

正如您所看到的,返回类型是 t,这里显示只是为了保持一致性。我希望能够返回一个新的目标(即怪物或玩家),其某些字段值已更改(例如生命值较少或具有新状态的新怪物)。问题是我不能只做到以下几点:

--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}

因为生命值、法力和状态“不是有效的记录选择器”。问题是我不知道 t 是怪物还是玩家,而且我不想指定“monsterHp”或“playerHp”,我想编写一个非常通用的函数。 我知道 Haskell Records 很笨拙并且没有太多可扩展性......

有什么想法吗?

再见,快乐编码,

阿尔弗雷多

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

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(3

静待花开 2024-12-12 05:32:53

就我个人而言,我认为哈马尔指出了玩家和怪物之间的相似之处,这是正确的。我同意您不想让它们相同,但请考虑这一点:采用此处的类型类...

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

...并将其替换为数据类型:

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

然后提取出共同点来自 PlayerMonster 的字段:

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

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

根据您对这些字段的处理方式,将其从内到外翻转可能更有意义:

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

...然后有 可定位玩家可定位怪物。这里的优点是,任何使用这两者的函数都可以采用 Targetable a 类型的东西——就像采用 Targetable 类的任何实例的函数一样。

这种方法不仅与您已有的方法几乎相同,而且代码也少了很多,并且使类型更简单(通过不在任何地方都有类约束)。事实上,上面的 Targetable 类型大致就是 GHC 在幕后为类型类创建的内容。

这种方法最大的缺点是它使访问字段变得更加笨拙——无论哪种方式,有些东西最终会变成两层深,并且将此方法扩展到更复杂的类型可以将它们嵌套得更深。造成这种尴尬的很多原因是字段访问器在语言中不是“一流的”——你不能像函数一样传递它们,对它们进行抽象,或者类似的东西。最流行的解决方案是使用“镜头”,另一个答案已经提到了。我通常使用 fclabels,所以这就是我的推荐。

我建议的分解类型与透镜的策略性使用相结合,应该为您提供比类型类方法更易于使用的东西,并且不会像拥有大量记录类型那样污染命名空间。

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.

青萝楚歌 2024-12-12 05:32:53

我可以提出三种可能的解决方案。

1)你的类型非常像OO,但是Haskell也可以用参数表达“sum”类型:

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

在OO设计中类似的东西通常会在Haskell中变成带有参数的“sum”类型。

2)您可以按照卡斯顿的建议进行操作,并将所有方法添加到类型类中。

3) 您可以将 Targetable 中的只读方法更改为公开获取和设置的“镜头”。请参阅堆栈溢出讨论。如果你的类型等级返回了镜片,那么你的法术伤害就可以应用。

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.

说不完的你爱 2024-12-12 05:32:53

为什么不将类似的函数包含

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

到类型类中呢?

Why don't you just include functions like

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

into your type-class?

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文