2012-09-03 1 views
7

J'écris une simulation d'un jeu de cartes dans Haskell wih plusieurs adversaires IA. Je voudrais avoir une fonction principale avec quelque chose comme GameParameters -> [AIPlayer] -> GameOutcome. Mais je voudrais penser à la fonction principale comme une "fonction de bibliothèque" afin que je puisse écrire un nouvel AIPlayer en modifiant n'importe quoi d'autre.Pluggable AI dans Haskell

donc je pensais que la création d'un AIPlayer de classe de types, de sorte que la fonction principale devient AIPlayer a => GameParameters -> [a] -> GameOutcome. Mais cela ne permet qu'un type d'IA à insérer. Donc, pour insérer plusieurs AIPlayers dans un jeu, je dois définir un wrappertype

AIWrapper = P1 AIPlayer1 | P2 AIPlayer2 | ... 

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

Je ne me sens pas heureux avec ce type d'emballage et je me sens comme il doit y avoir quelque chose de mieux que cela, ou je me trompe?

+1

Peut-être ce que vous cherchez est une collection hétérogène. Voir l'exemple 'Showable' à http://www.haskell.org/haskellwiki/Heterogenous_collections – ErikR

+2

Voir https://lukepalmer.wordpress.com/2010/01/24/haskell-antipattern-existential-typeclass/ pour une discussion sur comment transformer un besoin de listes hétérogènes en un bon design. –

Répondre

17

Il semble que vous pourriez être sur votre chemin à ce que Luc Palmer appelle le existential type class antipattern. Tout d'abord, supposons que vous avez deux types de données AI-jeu:

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

Maintenant, vous voulez travailler avec ceux-ci en les rendant les deux instances d'une classe de type:

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} 

Cette volonté travailler si vous avez seulement une telle valeur, mais peut vous causer des problèmes, comme vous l'avez remarqué. Qu'est-ce que la classe de type vous achète, cependant? Dans votre exemple, vous souhaitez traiter uniformément une collection d'instances différentes de AIPlayer. Puisque vous ne savez pas quels types spécifiques seront dans la collection, vous ne serez jamais en mesure d'appeler quelque chose comme gFactor ou rSeed; vous ne pourrez utiliser que les méthodes fournies par AIPlayer. Donc, tout ce dont vous avez besoin est une collection de ces fonctions, et nous pouvons emballer les dans une plaine ancien type de données:

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 } 

Un AIPlayer, alors, est une collection de savoir-faire: son nom, comment faire un mouvement, et comment apprendre et produire un nouveau joueur IA. Vos types de données et leurs instances deviennent simplement des fonctions qui produisent AIPlayer s; vous pouvez facilement mettre tout dans une liste, comme [greedy "Alice" 0.5, random "Bob" 42] est bien typé: il est de type [AIPlayer].


Vous pouvez , il est vrai, package votre premier cas avec un type existentiel:

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

Maintenant, [AIWrapper $ AIPlayerGreedy "Alice" 0.5, AIWrapper $ AIPlayerRandom "Bob" 42] est bien typé: il est de type [AIWrapper]. Mais comme le remarque de Luke Palmer ci-dessus, cela ne vous achète rien et, en fait, vous complique la vie. Comme c'est l'équivalent du cas le plus simple, sans type de classe, il n'y a aucun avantage; Les existentiels ne sont nécessaires que si la structure que vous enveloppez est plus compliquée.

+3

+1 Très belle explication! – Landei

+0

merci, c'est exactement le genre d'explication que je cherchais – Ingdas

Questions connexes