[英]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是笨拙的,並沒有太多的可擴展性......
任何的想法?
再見,快樂的編碼,
阿爾弗雷多
就我個人而言,我認為哈馬爾在正確的軌道上指出了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
類的任何實例的函數一樣。
這不僅是方法幾乎相同,你有什么已經,它也少了很多代碼,並保持類型簡單(因為不必到處類的限制)。 事實上,上面的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.