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

Alf*_*oli 5 polymorphism haskell records

这个帖子是这个帖子的后续

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

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
Run Code Online (Sandbox Code Playgroud)

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

--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])
Run Code Online (Sandbox Code Playgroud)

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

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

如您所见,返回类型是 t,此处显示只是为了保持一致。我希望能够返回一个新的目标对象(即一个怪物或一个玩家),并改变一些字段值(例如一个新的 hp 更少的怪物,或者一个新的状态)。问题是我不能只做到以下几点:

--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}
Run Code Online (Sandbox Code Playgroud)

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

任何的想法?

再见,快乐编码,

阿尔弗雷多

C. *_*ann 5

就个人而言,我认为 hammar 是在正确的轨道上指出Player和之间的相似之处Monster。我同意你不想让它们相同,但考虑一下:选择你在这里的类型类......

class Targetable a where
    name :: a -> String
    level :: a -> Int
    hp :: a -> HitPoints
    mp :: a -> ManaPoints
    status :: a -> Maybe [Status]
Run Code Online (Sandbox Code Playgroud)

...并用数据类型替换它:

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

然后从Player和 中提取出公共字段Monster

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

data Player = Player { playerTarget :: Targetable } deriving (Eq, Read, Show)
Run Code Online (Sandbox Code Playgroud)

根据您对这些的处理方式,将其翻过来可能更有意义:

data Targetable a = Targetable { target :: a
                               , name   :: String
                               -- &c...
                               }
Run Code Online (Sandbox Code Playgroud)

...然后有Targetable PlayerTargetable Monster。这里的优点是任何与这两者一起使用的函数都可以采用类型的东西——Targetable a就像采用Targetable类的任何实例的函数一样。

这不仅是方法几乎相同,你有什么已经,它也是一个很多更少的代码,并保持类型简单(因为不必到处类的限制)。实际上,Targetable上面的类型大致就是 GHC 在幕后为类型类创建的。

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

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