7

我正在用多个 AI 对手在 Haskell 中编写纸牌游戏的模拟。我想有一个类似的主要功能GameParameters -> [AIPlayer] -> GameOutcome。但我想将 main 函数视为“库函数”,这样我就可以编写一个新的 AIPlayer 来更改其他任何内容。

于是想到了创建一个typeclass AIPlayer,所以main函数变成了AIPlayer a => GameParameters -> [a] -> GameOutcome. 但这仅允许插入一种类型的 AI。所以要在一个游戏中插入多个 AIPlayers,我需要定义一个 wrappertype

AIWrapper = P1 AIPlayer1 | P2 AIPlayer2 | ...

instance AIWrapper AIPlayer where
     gameOperation (P1 x) = gameOperation x
     gameOperation (P2 x) = gameOperation x
     ...

我对这种包装类型不满意,我觉得肯定有比这更好的东西,还是我错了?

4

1 回答 1

17

听起来您可能正在使用 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统一处理不同实例的集合。由于您不知道集合中将包含哪些特定类型,因此您将永远无法调用类似gFactoror 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 玩家。您的数据类型及其实例只是成为产生AIPlayers 的函数;您可以轻松地将所有内容放在一个列表中,因为[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 上面的帖子所观察到的,这实际上并没有给你带来任何好处,实际上让你的生活变得更加复杂。因为它等价于更简单的无类型类案例,所以没有优势;仅当您要包装的结构更复杂时,才需要存在主义。

于 2012-09-04T00:05:23.753 回答