Haskell中的可插入AI

时间:2012-09-03 22:44:30

标签: inheritance haskell

我正在Haskell用多个AI对手写一个卡片游戏的模拟。 我希望有一个像GameParameters -> [AIPlayer] -> GameOutcome这样的主要功能。但是我想将主要功能看作是一个“库函数”,所以我可以编写一个新的AIPlayer来改变其他任何东西。

所以我考虑创建一个类型类AIPlayer,因此主函数变为AIPlayer a => GameParameters -> [a] -> GameOutcome。但这只允许插入一种类型的AI。因此,要在一个游戏中插入多个AIPlayers,我需要定义一个包装类型

AIWrapper = P1 AIPlayer1 | P2 AIPlayer2 | ...

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

我对这种包装类型感到不满意,我觉得必须有比这更好的东西,或者我错了吗?

1 个答案:

答案 0 :(得分:17)

听起来你可能正在前往Luke Palmer称之为existential type class antipattern的路上。首先,我们假设您有几种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的不同实例的集合。由于您不知道集合中将包含哪些特定的类型,因此您永远无法调用gFactorrSeed之类的内容。你只能使用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 }

然后,AIPlayer是一系列专有技术:它的名称,如何移动,以及如何学习和制作新的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的帖子所述,这实际上并没有给你带来什么,实际上会让你的生活更加复杂。因为它相当于更简单,没有类型的情况,所以没有优势;只有当你收尾的结构更复杂时才需要存在。