Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Pluggable AI in Haskell

I'm writing a simulation of a card game in Haskell wih multiple AI opponents. I'd like to have a main function with someting like GameParameters -> [AIPlayer] -> GameOutcome. But I'd like to think of the main function as a "library-function" so I can write a new AIPlayer with altering anything else.

So I thought of creating a typeclass AIPlayer, so the main function becomes AIPlayer a => GameParameters -> [a] -> GameOutcome. But this only allows for one type of AI to be inserted. So to insert multiple AIPlayers in one game, I need to define a wrappertype

AIWrapper = P1 AIPlayer1 | P2 AIPlayer2 | ...

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

I don't feel happy with this wrapper type and I feel like there must be something better than this, or am I wrong?

like image 887
Ingdas Avatar asked Sep 03 '12 22:09

Ingdas


1 Answers

It sounds like you might be on your way to what Luke Palmer termed the existential type class antipattern. First, let's suppose that you have a couple of AI-playing data types:

data AIPlayerGreedy = AIPlayerGreedy { gName :: String, gFactor :: Double }
data AIPlayerRandom = AIPlayerRandom { rName :: String, rSeed   :: Int }

Now, you want to work with these by making them both instances of a type class:

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}

This will work if you only have one such value, but can cause you to run into problems, as you noticed. What does the type class buy you, though? In your example, you want to treat a collection of different instances of AIPlayer uniformly. Since you don't know which specific types will be in the collection, you'll never be able to call anything like gFactor or rSeed; you'll only ever be able to use the methods provided by AIPlayer. So all you need is a collection of those functions, and we can package those up in a plain old data type:

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 }

An AIPlayer, then, is a collection of know-how: its name, how to make a move, and how to learn and produce a new AI player. Your data types and their instances simply become functions which produce AIPlayers; you can easily put everything in a list, as [greedy "Alice" 0.5, random "Bob" 42] is well-typed: it's of type [AIPlayer].


You can, it's true, package up your first case with an existential type:

{-# 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

Now, [AIWrapper $ AIPlayerGreedy "Alice" 0.5, AIWrapper $ AIPlayerRandom "Bob" 42] is well-typed: it's of type [AIWrapper]. But as Luke Palmer's post above observes, this doesn't actually buy you anything, and in fact makes your life more complicated. Since it's equivalent to the simpler, no–type-class case, there's no advantage; existentials are only necessary if the structure you're wrapping up is more complicated.

like image 179
Antal Spector-Zabusky Avatar answered Oct 20 '22 21:10

Antal Spector-Zabusky