听起来您可能正在使用 Luke Palmer 所说的存在类型类反模式。首先,假设您有几个 AI 播放数据类型:
data AIPlayerGreedy = AIPlayerGreedy { gName :: String, gFactor :: Double }
data AIPlayerRandom = AIPlayerRandom { rName :: String, rSeed :: Int }
现在,您希望通过使它们成为类型类的两个实例来使用它们:
class AIPlayer a where
name :: a -> String
makeMove :: a -> GameState -> GameState
learn :: a -> GameState -> a
instance AIPlayer AIPlayerGreedy where
name ai = gName ai
makeMove ai gs = makeMoveGreedy (gFactor ai) gs
learn ai _ = ai
instance AIPlayer AIPlayerRandom where
name ai = rName ai
makeMove ai gs = makeMoveRandom (rSeed ai) gs
learn ai gs = ai{rSeed = updateSeed (rSeed ai) gs}
如果您只有一个这样的值,这将起作用,但正如您所注意到的那样,可能会导致您遇到问题。但是,类型类给你带来了什么?在您的示例中,您希望AIPlayer
统一处理不同实例的集合。由于您不知道集合中将包含哪些特定类型,因此您将永远无法调用类似gFactor
or rSeed
; 您将只能使用AIPlayer
. 所以你所需要的只是这些函数的集合,我们可以将它们打包成一个普通的旧数据类型:
data AIPlayer = AIPlayer { name :: String
, makeMove :: GameState -> GameState
, learn :: GameState -> AIPlayer }
greedy :: String -> Double -> AIPlayer
greedy name factor = player
where player = AIPlayer { name = name
, makeMove = makeMoveGreedy factor
, learn = const player }
random :: String -> Int -> AIPlayer
random name seed = player
where player = AIPlayer { name = name
, makeMove = makeMoveRandom seed
, learn = random name . updateSeed seed }
因此, AnAIPlayer
是技术诀窍的集合:它的名称、如何移动,以及如何学习和产生新的 AI 玩家。您的数据类型及其实例只是成为产生AIPlayer
s 的函数;您可以轻松地将所有内容放在一个列表中,因为[greedy "Alice" 0.5, random "Bob" 42]
它的类型是正确的:它的类型是[AIPlayer]
.
确实,您可以使用存在类型打包您的第一个案例:
{-# LANGUAGE ExistentialQuantification #-}
data AIWrapper = forall a. AIPlayer a => AIWrapper a
instance AIWrapper a where
makeMove (AIWrapper ai) gs = makeMove ai gs
learn (AIWrapper ai) gs = AIWrapper $ learn ai gs
name (AIWrapper ai) = name ai
现在,[AIWrapper $ AIPlayerGreedy "Alice" 0.5, AIWrapper $ AIPlayerRandom "Bob" 42]
它的类型很好:它是[AIWrapper]
. 但正如 Luke Palmer 上面的帖子所观察到的,这实际上并没有给你带来任何好处,实际上让你的生活变得更加复杂。因为它等价于更简单的无类型类案例,所以没有优势;仅当您要包装的结构更复杂时,才需要存在主义。