1 класс против данных
Существо должно быть классом - оно описывает интерфейс. Данные следует использовать, когда вы думаете о фактическом сообщении значений или когда вам нужно ввести новый тип, оборачивая существующий объект новым поведением. Например, монаде Identity
необходимо обернуть ее значения в новый тип, иначе вы увидите instance Monad a
для всех a
, что может привести к конфликту с созданием чего-либо еще Monad
экземпляра. Но, возможно, вам придется обернуть его.
2 списка
Есть способ сделать это с Data.Dynamic
, но каждый раз, когда я думал о том, чтобы сделать это таким образом, я мог придумывать способ сделать это с обычными классами типов вместо этого. Тем не менее, я не так много написал на Haskell, и многие библиотеки, безусловно, полагаются на Data.Dynamic
. Если вы действительно хотите распаковать тип, вам, вероятно, нужно его использовать.
3 экстенсиональность
Как и раньше, если вы можете оставить функциональность, зависящую от типа, в классах, это лучше всего. Было бы очень полезно, если бы вы могли опубликовать пример, показывающий, почему вы не можете добавить другую функцию в Creature
. Я предполагаю, что вы хотите сосчитать numParrots
в приведенном ниже примере, и вам действительно нужно их распаковать.
4 общих комментария
Всегда есть много решений проблемы. Исходя из вашего описания, я думаю, что «разные миры должны повлечь за собой разные типы сообщений», а не то, что мир должен быть привязан к определенному типу существ (например, ParrotWorld
).
другое решение
вот мое решение, используя Data.Typeable
. Как уже упоминалось выше, я впервые использую его, так что, возможно, есть более чистый способ.
{-# LANGUAGE DeriveDataTypeable,
ImpredicativeTypes,
NoMonomorphismRestriction,
RankNTypes,
ScopedTypeVariables #-}
module Test where
import Data.Typeable
type Message = String
class Typeable α => Creature α where
processInput :: α -> Message -> Message
-- box a creature
type BoxedC = (Message -> Message, Typeable β => Maybe β)
boxC :: Creature α => α -> BoxedC
boxC x = (processInput x, cast x)
class World α where
-- from your description, I'd not have Creature as part of this.
processAction :: α -> Message -> α
getCreatures :: α -> [BoxedC]
data Parrot = Parrot { parrotMessage :: String } deriving Typeable
data Lizard = Lizard { lizardMessage :: String } deriving Typeable
instance Creature Parrot where processInput p _ = (parrotMessage p)
instance Creature Lizard where processInput l _ = (lizardMessage l)
-- NOTE: Keep it simple and use a single World instance
-- (i.e. no typeclass) unless you need it.
data BaseWorld = BaseWorld { creatureList :: [BoxedC] }
instance World BaseWorld where
processAction w _ = w
getCreatures = creatureList
w = BaseWorld [boxC $ Parrot "parrot1", boxC $ Lizard "Lizard1"]
numParrots :: [BoxedC] -> Int
numParrots lst = foldl (+) 0 (map (go . snd) lst) where
go :: (forall β. Typeable β => Maybe β) -> Int
go (Just x :: Maybe Parrot) = 1
go _ = 0
test = numParrots (getCreatures w)
Идея похожа на вашу: мы помещаем существ в коробку, прежде чем поместить их в список. Элементы в штучной упаковке имеют достаточно данных, чтобы вы могли распаковать тип, если вам нужно. И последнее, что нужно упомянуть, хотя, возможно, это не то, что вам нужно, это то, что замыкания являются мощными. Вам не нужно вести список существ, если вы можете выразить их результаты в виде композиции функций. Например, в псевдокоде у вас может быть функция
bind_creature :: Creature -> World -> World
, который добавляет существо в мир, и у World есть тип, который возвращает его следующую итерацию,
data World = World { nextWorld :: World }
, который вы установили себе для базы, а именно w = World w
. Для простоты предположим, что у каждого существа есть функция
transformWorld :: Creature -> World -> World
тогда вы можете реализовать bind_creature, как,
bind_creature c w = World { nextWorld = transformWorld c (nextWorld w) }
надеюсь, это поможет.