簡體   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