2012-02-05 1 views
5

Vorrei utilizzare haskell per implementare un gioco e vorrei utilizzare un sistema di classi di tipi per implementare il sistema di articoli. Avrebbe funzionato o meno così:Ispezione Haskell Typeclass

data Wood = Wood Int 

instance Item Wood where 
    image a = "wood.png" 
    displayName a = "Wood" 

instance Flammable Wood where 
    burn (Wood health) | health' <= 0 = Ash 
        | otherwise = Wood health' 
     where health' = health - 100 

dove la voce e le classi infiammabili sono qualcosa di simile:

class Item a where 
    image :: a -> String 
    displayName :: a -> String 

class Flammable a where 
    burn :: (Item b) => a -> b 

Per fare questo, avrei bisogno di un modo per rilevare se un valore è un'istanza di una classe di tipo.

Il modulo Data.Data offre una funzionalità simile che mi consente di credere che ciò sia possibile.

+2

Non sono sicuro che quello che stai facendo si inserisca nel modello di Haskell. Che un valore sia un'istanza di una classe di tipo dovrebbe essere dimostrabile staticamente. – millimoose

+4

I valori non possono essere istanze di classi di tipi. I tipi sono istanze di classi di tipi. –

+4

Vedere [questa domanda frequente] (http://www.haskell.org/haskellwiki/FAQ#I.27m_making_an_RPG._Should_I_define_a_type_for_each_kind_of_monster.2C_and_a_type_class_for_them.3F). – ehird

risposta

8

Le classi di tipo sono probabilmente il modo sbagliato di andare qui. Considerare l'utilizzo di tipi di dati algebrici semplici, invece, ad esempio:

data Item = Wood Int | Ash 

image Wood = "wood.png" 
image Ash = ... 

displayName Wood = "Wood" 
displayName Ash = "Ash" 

burn :: Item -> Maybe Item 
burn (Wood health) | health' <= 0 = Just Ash 
        | otherwise = Just (Wood health') 
    where health' = health - 100 
burn _ = Nothing -- Not flammable 

Se questo lo rende troppo difficile aggiungere nuovi elementi, è possibile invece di codificare le operazioni nel tipo di dati in sé.

data Item = Item { image :: String, displayName :: String, burn :: Maybe Item } 

ash :: Item 
ash = Item { image = "...", displayName = "Ash", burn :: Nothing } 

wood :: Int -> Item 
wood health = Item { image = "wood.png", displayName = "Wood", burn = Just burned } 
    where burned | health' <= 0 = ash 
       | otherwise = wood health' 
      health' = health - 100 

Tuttavia, questo rende più difficile aggiungere nuove funzioni. Il problema di eseguire entrambi allo stesso tempo è noto come expression problem. C'è uno nice lecture on Channel 9 by Dr. Ralf Lämmel dove spiega questo problema in modo più approfondito e discute varie non-soluzioni, vale la pena controllare se hai tempo.

Ci sono degli approcci per risolverlo, ma sono considerevolmente più complessi dei due progetti che ho illustrato, quindi raccomando di usare uno di questi se soddisfa le tue esigenze, e non preoccuparti del problema dell'espressione a meno che tu non debba .

+0

Il primo potrebbe funzionare (anche se non penso che sarebbe senza un DataData hack), ma renderebbe più difficile aggiungere più elementi (e ce ne sono oltre 200). Il secondo non funzionerebbe affatto perché alcuni articoli non sono infiammabili. Quello che voglio dire è che potresti avere "Legno" che è infiammabile e può essere messo in inventario e "Zombi" che è infiammabile e può combattere. Questi due elementi condividono un tratto comune, ma hanno anche un altro tratto che l'altro non ha. – adrusi

+0

Questo è il motivo per cui ho voluto utilizzare le classi di tipi, l'ostacolo è che avrei dovuto essere in grado di testare, ad esempio, se l'elemento con cui il lettore interagisce è infiammabile, il che significa testare se è un membro di una classe . – adrusi

+0

@adrusi: Se qualcosa è un membro di una classe di tipo è una proprietà in fase di compilazione, e quindi non è proprio adatto a ciò che stai cercando di fare. Da quello che stai descrivendo, sembra che il secondo approccio dovrebbe funzionare, anche se potresti aver bisogno di alcune modifiche. Ciò dipende da quali proprietà sono in comune tra determinati tipi di elementi. Ad esempio, se un elemento può avere una qualsiasi combinazione di proprietà, puoi semplicemente avere un campo 'Maybe' per ogni proprietà, come quella che ho per gli oggetti infiammabili. Gli oggetti che non hanno quella proprietà avranno solo un 'Nothing' there. – hammar

5

Ecco il problema:

burn :: (Item b) => a -> b 

Ciò significa che il valore del risultato di burn deve essere polimorfico. Deve essere in grado di riempire qualsiasi buco per qualsiasi istanza di Item.

Ora, è abbastanza evidente che si sta cercando di scrivere qualcosa di simile (in linguaggio OO immaginario con interfacce e sottoclassi):

Interface Item { 
    String getImage(); 
    String getDisplayName(); 
} 

Interface Flammable { 
    Item burn(); 
} 

In questa sorta di codice, stai dicendo che burn produrrà qualche articolo, senza alcuna garanzia circa che tipo dell'articolo è. Questa è la differenza tra "per tutti" e "esiste". Quello che volevi esprimere nel codice Haskell era "esiste", ma ciò che hai espresso in realtà era "per tutti".

Ora se sei davvero sicuro di voler fare "esiste" la funzionalità, puoi dare un'occhiata usando Existential Types. Ma attenzione. Se avete intenzione di scrivere codice come questo:

if (foo instanceof Flammable) { 
    ... 
} 

Poi si sono quasi certamente facendo male, e vi imbatterete in tanto dolore e l'agonia. Considera invece le alternative suggerite da hammar.