简体   繁体   English

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

[英]Haskell polymorphic functions with records and class types

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. 我正在实现一个简单的战斗系统作为玩具项目,这是你可以在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. 正如您所看到的,返回类型为t,这里只显示了一致性。 I want be able to return a new targetable (ie 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". 因为hp,mana和status“不是有效的记录选择器”。 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. 问题是我不知道先验是否是怪物或玩家,而且我不想指定“monsterHp”或“playerHp”,我想写一个非常通用的函数。 I know that Haskell Records are clumsy and not much extensibile... 我知道Haskell Records是笨拙的,并没有太多的可扩展性......

Any idea? 任何的想法?

Bye and happy coding, 再见,快乐的编码,

Alfredo 阿尔弗雷多

Personally, I think hammar is on the right track with pointing out the similarities between Player and Monster . 就我个人而言,我认为哈马尔在正确的轨道上指出了PlayerMonster之间的相似之处。 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 : 然后分解出PlayerMonster的常见字段:

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

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. 事实上,上面的Targetable类型大致是GHC在类型类的幕后创建的。

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. 我通常使用fclabels ,所以这是我的建议。

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

I can suggest three possible solutions. 我可以建议三种可能的解决方案

1) Your types are very OO-like, but Haskell can also express "sum" types with parameters: 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

Thing that are similar in OO-design often become "sum" types with parameters in Haskell. 在OO设计中类似的东西经常在Haskell中变成带有参数的“sum”类型。

2) You can do what Carston suggests and add all your methods to type classes. 2)您可以执行Carston建议的操作,并将所有方法添加到类型类中。

3) You can change your read-only methods in Targetable to be "lenses" that expose both getting and setting. 3)您可以将Targetable中的只读方法更改为显示获取和设置的“镜头”。 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 你为什么不只是包括像

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

into your type-class? 进入你的类型?

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

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