2012-09-03 17 views
7

Estoy escribiendo una simulación de un juego de cartas en Haskell con múltiples oponentes AI. Me gustaría tener una función principal con algo como GameParameters -> [AIPlayer] -> GameOutcome. Pero me gustaría pensar en la función principal como una "función de biblioteca" para poder escribir una nueva AIPlayer con la alteración de cualquier otra cosa.AI conectable en Haskell

Así que pensé en crear una clase de tipo AIPlayer, por lo que la función principal es AIPlayer a => GameParameters -> [a] -> GameOutcome. Pero esto solo permite insertar un tipo de IA. Así que para insertar múltiples AIPlayers en un juego, necesito definir un wrappertype

AIWrapper = P1 AIPlayer1 | P2 AIPlayer2 | ... 

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

no me siento feliz con este tipo de envoltura y siento que debe haber algo mejor que esto, o estoy equivocado?

+1

Quizás lo que está buscando es una colección heterogénea. Vea el ejemplo 'Showable' en http://www.haskell.org/haskellwiki/Heterogenous_collections – ErikR

+2

Consulte https://lukepalmer.wordpress.com/2010/01/24/haskell-antipattern-existential-typeclass/ para una discusión de cómo transformar la necesidad de listas heterogéneas en un buen diseño. –

Respuesta

17

Parece que podría estar en camino hacia lo que Luke Palmer denominó existential type class antipattern. En primer lugar, vamos a suponer que usted tiene un par de tipos de datos AI-playing:

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

Ahora, usted quiere trabajar con ellos haciéndoles ambas instancias de una clase de tipo:

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} 

Esta voluntad funciona si solo tienes uno de esos valores, pero puedes causarte problemas, como habrás notado. Sin embargo, ¿qué te compra la clase de tipo? En su ejemplo, desea tratar una colección de diferentes instancias de AIPlayer uniformemente. Como no sabe qué tipos de específicos estarán en la colección, nunca podrá llamar a nada como gFactor o rSeed; solo podrá utilizar los métodos provistos por AIPlayer. Así que todo lo que necesita es una colección de esas funciones, y podemos empaquetar los en una llanura de edad tipo de datos:

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, entonces, es una colección de conocimientos: su nombre, ¿cómo para hacer un movimiento, y cómo aprender y producir un nuevo jugador AI. Sus tipos de datos y sus instancias simplemente se convierten en funciones que producen AIPlayer s; puede poner todo fácilmente en una lista, ya que [greedy "Alice" 0.5, random "Bob" 42] está bien tipado: es del tipo [AIPlayer].


Usted puede , es verdad, el paquete de su primer caso con un tipo existencial:

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

Ahora, [AIWrapper $ AIPlayerGreedy "Alice" 0.5, AIWrapper $ AIPlayerRandom "Bob" 42] está bien mecanografiadas-: es de tipo [AIWrapper]. Pero como observa la publicación de Luke Palmer, esto en realidad no te compra nada, y de hecho hace tu vida más complicada. Dado que es equivalente al caso más simple, sin clase, no hay ventaja; los existenciales solo son necesarios si la estructura que está terminando es más complicada.

+3

+1 ¡Muy buena explicación! – Landei

+0

gracias, ese es exactamente el tipo de explicación que estaba buscando – Ingdas