这篇文章是this one.
的以下内容我正在实现一个简单的战斗系统作为玩具项目,这是你可以在Final Fantasy et simila等游戏中找到的典型系统。我用类型+自定义实例解决了臭名昭着的“命名空间污染”问题。例如:
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}
因为hp,mana和status“不是有效的记录选择器”。问题是我不知道先验是否是怪物或玩家,而且我不想指定“monsterHp”或“playerHp”,我想写一个非常通用的函数。 我知道Haskell Records是笨拙的,并没有太大的可扩展性......
有什么想法吗?
再见,快乐的编码,
Alfredo
答案 0 :(得分:4)
就我个人而言,我认为hammar在正确的轨道上指出了Player
和Monster
之间的相似之处。我同意你不想让它们成为相同的,但请考虑一下:参加你在这里的类型课程......
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)
然后将Player
和Monster
中的公共字段分解出来:
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 Player
和Targetable Monster
。这里的优点是任何与其中任何一个一起使用的函数都可以采用类型为Targetable a
的函数 - 就像可以使用Targetable
类的任何实例的函数一样。
这种方法不仅几乎与您已经拥有的方法相同,而且还是 lot 更少的代码,并且使类型更简单(通过不在任何地方使用类约束)。实际上,上面的Targetable
类型大致是GHC在类型类的幕后创建的。
这种方法最大的缺点是它使得访问字段变得更加笨拙 - 无论哪种方式,有些东西最终都是两层深度,并且将这种方法扩展到更复杂的类型可以更深入地嵌套它们。很多令人尴尬的事实是,字段访问器不是语言中的“第一类” - 你不能像函数一样传递它们,抽象它们或类似的东西。最流行的解决方案是使用“镜头”,这已经提到了另一个答案。我通常使用the fclabels
package来做这个,所以这是我的建议。
我建议的分解类型,结合镜头的策略使用,应该给你一些比类型类方法更简单的东西,并且不会像许多记录类型那样污染命名空间。
答案 1 :(得分:3)
我可以提出三种可能的解决方案。
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)您可以执行Carston建议的操作,并将所有方法添加到类型类中。
3)您可以将Targetable中的只读方法更改为显示获取和设置的“镜头”。请参阅stack overflow discussion。如果您的类型类别返回镜头,则可能会使您的法术伤害适用。
答案 2 :(得分:1)
为什么不直接包含
等功能InflicteDamage :: a -> Int -> a
AddStatus :: a -> Status -> a
进入你的类型类?