2012-09-03 17 views
7

Ich schreibe eine Simulation eines Kartenspiels in Haskell mit mehreren KI-Gegnern. Ich hätte gerne eine Hauptfunktion mit etwas wie GameParameters -> [AIPlayer] -> GameOutcome. Aber ich möchte mir die Hauptfunktion als "Bibliotheksfunktion" vorstellen, damit ich einen neuen AIPlayer schreiben kann, der etwas anderes verändert.Pluggable AI in Haskell

Also dachte ich über das Erstellen einer Typklasse AIPlayer, so dass die Hauptfunktion AIPlayer a => GameParameters -> [a] -> GameOutcome wird. Dies erlaubt jedoch nur das Einfügen einer Art von AI. So mehrere AIPlayers in einem Spiel einzufügen, muss ich ein wrappertype

AIWrapper = P1 AIPlayer1 | P2 AIPlayer2 | ... 

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

Ich fühle mich nicht mit diesem Wrapper glücklich definieren, und ich fühle mich wie es etwas besser als diese sein muss, oder bin ich falsch?

+1

Vielleicht, was Sie suchen, ist eine heterogene Sammlung. Siehe das Beispiel 'Showable' unter http://www.haskell.org/haskellwiki/Heterogenic_collections – ErikR

+2

Siehe https://lukepalmer.wordpress.com/2010/01/24/haskell-antipattern-existential-typeclass/ für eine Diskussion von Wie man eine Notwendigkeit für heterogene Listen in ein gutes Design umwandelt. –

Antwort

17

Es klingt, als ob Sie auf dem Weg zu dem sein könnten, was Luke Palmer existential type class antipattern nannte. Zuerst nehmen wir an, dass Sie ein paar KI-Spieldatentypen haben:

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

Jetzt wollen Sie mit diesen arbeiten, indem sie beide Instanzen einer Typklasse machen:

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} 

Dieser Wille funktionieren, wenn Sie nur einen solchen Wert haben, aber können Sie Probleme verursachen, wie Sie bemerkt haben. Was kauft dir die Typklasse? In Ihrem Beispiel möchten Sie eine Sammlung verschiedener Instanzen von AIPlayer einheitlich behandeln. Da Sie nicht wissen, welche spezifischen Typen in der Sammlung sein werden, können Sie nie etwas wie gFactor oder rSeed aufrufen; Sie können nur die von AIPlayer bereitgestellten Methoden verwenden. Also alles, was Sie brauchen, ist eine Sammlung dieser Funktionen, und wir können diejenigen oben in einem einfachen alten Datentyp Paket:

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 } 

Ein AIPlayer, dann ist eine Sammlung von Know-how: seinen Namen, wie um einen Zug zu machen, und wie man einen neuen KI-Spieler lernt und produziert. Ihre Datentypen und ihre Instanzen werden einfach zu Funktionen, die AIPlayer s ergeben; Sie können einfach alles in eine Liste eintragen, da [greedy "Alice" 0.5, random "Bob" 42] gut typisiert ist: es ist vom Typ [AIPlayer].


Sie können, es ist wahr, Paket Ihren ersten Fall mit einer existentiellen Typ:

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

Nun [AIWrapper $ AIPlayerGreedy "Alice" 0.5, AIWrapper $ AIPlayerRandom "Bob" 42] ist gut getippt: es ist vom Typ [AIWrapper]. Aber wie Luke Palmers Beitrag oben bemerkt, kauft dir das eigentlich nichts und macht dein Leben sogar komplizierter. Da es dem einfacheren Fall der Nicht-Klassenklasse entspricht, gibt es keinen Vorteil. Existenzen sind nur notwendig, wenn die Struktur, die du einwickelst, komplizierter ist.

+3

+1 Sehr schöne Erklärung! – Landei

+0

danke, das ist genau die Art von Erklärung, die ich suchte – Ingdas