具有记录和类类型的 Haskell 多态函数
这篇文章是这篇文章的后续内容。
我正在实现一场简单的战斗系统作为玩具项目,您可以在《最终幻想》等游戏中找到典型的系统。我已经用类类型+自定义实例解决了臭名昭著的“命名空间污染”问题。例如:
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 技术交流群。
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论
评论(3)
就我个人而言,我认为哈马尔指出了玩家和怪物之间的相似之处,这是正确的。我同意您不想让它们相同,但请考虑这一点:采用此处的类型类...
...并将其替换为数据类型:
然后提取出共同点来自
Player
和Monster
的字段:根据您对这些字段的处理方式,将其从内到外翻转可能更有意义:
...然后有
可定位玩家
和可定位怪物
。这里的优点是,任何使用这两者的函数都可以采用Targetable a
类型的东西——就像采用Targetable
类的任何实例的函数一样。这种方法不仅与您已有的方法几乎相同,而且代码也少了很多,并且使类型更简单(通过不在任何地方都有类约束)。事实上,上面的
Targetable
类型大致就是 GHC 在幕后为类型类创建的内容。这种方法最大的缺点是它使访问字段变得更加笨拙——无论哪种方式,有些东西最终会变成两层深,并且将此方法扩展到更复杂的类型可以将它们嵌套得更深。造成这种尴尬的很多原因是字段访问器在语言中不是“一流的”——你不能像函数一样传递它们,对它们进行抽象,或者类似的东西。最流行的解决方案是使用“镜头”,另一个答案已经提到了。我通常使用
fclabels
包,所以这就是我的推荐。我建议的分解类型与透镜的策略性使用相结合,应该为您提供比类型类方法更易于使用的东西,并且不会像拥有大量记录类型那样污染命名空间。
Personally, I think hammar is on the right track with pointing out the similarities between
Player
andMonster
. I agree you don't want to make them the same, but consider this: Take the type class you have here......and replace it with a data type:
Then factor out the common fields from
Player
andMonster
:Depending on what you do with these, it might make more sense to turn it inside-out instead:
...and then have
Targetable Player
andTargetable Monster
. The advantage here is that any functions that work with either can take things of typeTargetable a
--just like functions that would have taken any instance of theTargetable
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.
我可以提出三种可能的解决方案。
1)你的类型非常像OO,但是Haskell也可以用参数表达“sum”类型:
在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:
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
into your type-class?