繁体   English   中英

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

[英]Haskell polymorphic functions with records and class types

这篇文章的内容如下

我正在实现一个简单的战斗系统作为玩具项目,这是你可以在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是笨拙的,并没有太多的可扩展性......

任何的想法?

再见,快乐的编码,

阿尔弗雷多

就我个人而言,我认为哈马尔在正确的轨道上指出了PlayerMonster之间的相似之处。 我同意你不想让它们变得一样 ,但考虑一下:拿你在这里的类型课......

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 PlayerTargetable Monster 这里的优点是任何使用其中任何一个的函数都可以采用Targetable a类型的东西 - 就像可以使用Targetable类的任何实例的函数一样。

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

这种方法的最大缺点是它使得访问字段变得更加笨拙 - 无论哪种方式,有些东西最终都是两层深,而将这种方法扩展到更复杂的类型可以更深入地嵌套它们。 很多令人尴尬的事实是,字段访问器不是语言中的“第一类” - 你不能像函数一样传递它们,抽象它们或类似的东西。 最流行的解决方案是使用“镜头”,这已经提到了另一个答案。 我通常使用fclabels ,所以这是我的建议。

我建议的分解类型,结合镜头的策略使用,应该给你一些比类型类方法更简单的东西,并且不会像许多记录类型那样污染命名空间。

我可以建议三种可能的解决方案

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中的只读方法更改为显示获取和设置的“镜头”。 请参阅堆栈溢出讨论 如果您的类型类别返回镜头,那么它将使您的法术伤害可以应用。

你为什么不只是包括像

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

进入你的类型?

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

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