Полиморфные функции Haskell с записями и типами классов - PullRequest
4 голосов
/ 17 сентября 2011

это сообщение является следующим из этого.

Я представляю простую боевую систему как игрушечный проект, типичную систему, которую вы можете найти в таких играх, как 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», например:

--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, мана и статус "не действительный селектор записей". Проблема в том, что я априори не знаю, будет ли t монстром или игроком, и я не хочу указывать «monsterHp» или «playerHp», я хочу написать довольно общую функцию. Я знаю, что записи на Haskell неуклюжи и не слишком растяжимы ...

Есть идеи?

До свидания и счастливого кодирования,

Альфредо

Ответы [ 3 ]

4 голосов
/ 17 сентября 2011

Лично я думаю, что Хаммар находится на правильном пути, указывая на сходство между 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 , так что это моя рекомендация.

Факторизованные типы, которые я предлагаю, в сочетании со стратегическим использованием линз, должны дать вам то, чтопроще в использовании, чем подход с использованием классов типов, и не загрязняет пространство имен, как это делает множество типов записей.

3 голосов
/ 17 сентября 2011

Я могу предложить три возможных решения.

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

Вещи, которые похожи в ОО-дизайне, часто становятся типами "суммы" с параметрами в Haskell.

2) Вы можете делать то, что предлагает Карстон, и добавлять все свои методы в классы типов.

3) Вы можете изменить методы только для чтения в Targetable, чтобы они стали «линзами», которые предоставляют как получение, так и настройку. См. Обсуждение переполнения стека . Если ваш класс шрифта вернул линзы, это могло бы привести к нанесению урона вашим заклинанием.

1 голос
/ 17 сентября 2011

Почему бы вам просто не включить функции типа

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

в ваш класс типов?

...