2015-01-30 7 views
14

Mi piacerebbe scrivere un programma che stampa alcuni metadati di un tipo Haskell. Anche se so che questo non è un codice valido, l'idea è qualcosa di simile:Come posso leggere i metadati di un tipo in fase di esecuzione?

data Person = Person { name :: String, age :: Int } 

metadata :: Type -> String 
metadata t = ??? 

metadata Person -- returns "Person (name,age)" 

La limitazione importante è che non ho un'istanza di Person, proprio il tipo.

Ho iniziato a esaminare Generics & Typeable/Data, ma senza un'istanza non sono sicuro che faranno ciò di cui ho bisogno. Qualcuno può indicarmi la giusta direzione?

risposta

20

Reflection in Haskell funziona con la classe Typeable, definita in Data.Typeable e include il metodo typeOf * per ottenere una rappresentazione runtime del tipo di un valore.

ghci> :m +Data.Typeable 
ghci> :t typeOf 'a' 
typeOf 'a' :: TypeRep 
ghci> typeOf 'a' -- We could use any value of type Char and get the same result 
Char -- the `Show` instance of `TypeRep` just returns the name of the type 

Se si desidera Typeable lavorare per i propri tipi, si può avere il compilatore di generare un'istanza per voi con l'estensione DeriveDataTypeable.

{-# LANGUAGE DeriveDataTypeable #-} 
import Data.Typeable 
data Person = Person { name :: String, age :: Int } deriving Typeable 

È anche possibile scrivere il proprio esempio, ma in realtà, nessuno ha il tempo per questo. A quanto pare non si può - si vedano i commenti

è ora possibile utilizzare typeOf per afferrare una rappresentazione in fase di esecuzione del vostro tipo. Siamo in grado di richiedere informazioni sul tipo di costruzione (abbreviato in TyCon) ed i suoi argomenti di tipo:

-- (undefined :: Person) stands for "some value of type Person". 
-- If you have a real Person you can use that too. 
-- typeOf does not use the value, only the type 
-- (which is known at compile-time; typeOf is dispatched using the normal instance selection rules) 
ghci> typeOf (undefined :: Person) 
Person 
ghci> tyConName $ typeRepTyCon $ typeOf (undefined :: Person) 
"Person" 
ghci> tyConModule $ typeRepTyCon $ typeOf (undefined :: Person) 
"Main" 

Data.Typeable fornisce anche un type-safe fusione operazione che consente di ramo sul tipo di runtime di un valore, un po 'come C#' s as operatore.

f :: Typeable a => a -> String 
f x = case (cast x :: Maybe Int) of 
      Just i -> "I can treat i as an int in this branch " ++ show (i * i) 
      Nothing -> case (cast x :: Maybe Bool) of 
          Just b -> "I can treat b as a bool in this branch " ++ if b then "yes" else "no" 
          Nothing -> "x was of some type other than Int or Bool" 

ghci> f True 
"I can treat b as a bool in this branch yes" 
ghci> f (3 :: Int) 
"I can treat i as an int in this branch 9" 

Per inciso, un modo migliore di scrivere f è quello di utilizzare un GADT enumerare l'insieme dei tipi che ci si aspetta la funzione di essere chiamato con. Questo ci permette di perdere Maybe (f non può mai fallire!), Fa un lavoro migliore per documentare i nostri presupposti e fornisce un riscontro in fase di compilazione quando abbiamo bisogno di cambiare il set di tipi di argomenti ammissibili per f. (È possibile scrivere una classe per fare Admissible implicita, se volete.)

data Admissible a where 
    AdInt :: Admissible Int 
    AdBool :: Admissible Bool 
f :: Admissible a -> a -> String 
f AdInt i = "I can treat i as an int in this branch " ++ show (i * i) 
f AdBool b = "I can treat b as a bool in this branch " ++ if b then "yes" else "no" 

In realtà probabilmente non farebbe uno di questi - che avevo appena bastone f in una classe e definire le istanze per Int e Bool .


Se si desidera eseguire in tempo le informazioni sul lato destro di una definizione di tipo, è necessario utilizzare il entertainingly-nome Data.Data, che definisce una sottoclasse di Typeable chiamato Data.** GHC può derivare Data anche per te, con la stessa estensione:

{-# LANGUAGE DeriveDataTypeable #-} 
import Data.Typeable 
import Data.Data 
data Person = Person { name :: String, age :: Int } deriving (Typeable, Data) 

Ora possiamo afferrare una rappresentazione di run-time dei valori di un tipo, non solo il tipo stesso:

ghci> dataTypeOf (undefined :: Person) 
DataType {tycon = "Main.Person", datarep = AlgRep [Person]} 
ghci> dataTypeConstrs $ dataTypeOf (undefined :: Person) 
[Person] -- Person only defines one constructor, called Person 
ghci> constrFields $ head $ dataTypeConstrs $ dataTypeOf (undefined :: Person) 
["name","age"] 

Data.Data è l'API per la programmazione generica; se si sentono persone che parlano di "Scrap Your Boilerplate", questo (insieme a Data.Generics, che si basa su Data.Data) è ciò che intendono dire. Ad esempio, è possibile scrivere una funzione che converte i tipi di record in JSON utilizzando la riflessione sui campi del tipo.

toJSON :: Data a => a -> String 
-- Implementation omitted because it is boring. 
-- But you only have to write the boring code once, 
-- and it'll be able to serialise any instance of `Data`. 
-- It's a good exercise to try to write this function yourself! 

* Nelle versioni recenti di GHC, questa API è cambiato un po '. Consulta i documenti.

** Sì, il nome completo di tale classe è Data.Data.Data.

+1

Brillante! Grazie per l'eccellente spiegazione. Quel trucco '(undefined :: Person)' era particolarmente illuminante. Non avevo realizzato che esistesse un modo per ottenere un'istanza "vuota" del tipo. :-) –

+1

Funziona perché 'typeOf' in realtà non usa il valore del suo argomento. Abbiamo solo bisogno di dargli 'qualcosa' di un certo tipo in modo che GHC possa selezionare l'istanza giusta di 'Typeable'. –

+2

@KrisJenkins C'è molta contesa sul trucco "indefinito". Alcune persone pensano che sia sufficiente, altri pensano che sia un hack poiché usa "non definito". Si potrebbe implementare l'API per usare invece qualcosa come 'typeOf :: Typeable a => Proxy a -> TypeRep' e' typeOf (Proxy :: Proxy Person) 'che evita' indefinito', ma poi c'è disaccordo su come esattamente questo dovrebbe essere implementato anche Per la maggior parte dei casi d'uso, 'undefined' dovrebbe funzionare bene, ma evitare di utilizzare il valore attuale per qualsiasi cosa poiché genererebbe un'eccezione. – bheklilr

Problemi correlati