2010-09-16 11 views
10

Durante la scrittura di programmi più grandi in Haskell mi trovo a trovarmi di fronte a un problema comune. Mi trovo spesso a desiderare più tipi distinti che condividono una rappresentazione interna e diverse operazioni di base.Gestione di più tipi con la stessa rappresentazione interna e numero minimo di caratteri?

Ci sono due approcci relativamente ovvi per risolvere questo problema.

Uno sta utilizzando una classe di tipo e l'estensione GeneralizedNewtypeDeriving. Inserire una logica sufficiente in una classe di tipo per supportare le operazioni condivise che il caso d'uso desidera. Creare un tipo con la rappresentazione desiderata e creare un'istanza della classe del tipo per quel tipo. Quindi, per ogni caso d'uso, crea dei wrapper per esso con newtype e ricava la classe comune.

L'altro è dichiarare il tipo con una variabile di tipo fantasma, quindi utilizzare EmptyDataDecls per creare tipi distinti per ogni caso di utilizzo diverso.

La mia preoccupazione principale non è mescolare valori che condividono la rappresentazione e le operazioni interne, ma hanno significati diversi nel mio codice. Entrambi gli approcci risolvono questo problema, ma si sentono decisamente goffi. La mia seconda preoccupazione è la riduzione della quantità di piastra di riscaldamento richiesta, e entrambi gli approcci funzionano abbastanza bene.

Quali sono i vantaggi e gli svantaggi di ciascun approccio? Esiste una tecnica che si avvicini a fare ciò che voglio, fornendo sicurezza di tipo senza codice boilerplate?

risposta

2

Ho confrontato esempi di giocattoli e non ho trovato una differenza di prestazioni tra i due approcci, ma l'utilizzo di solito differisce leggermente.

Ad esempio, in alcuni casi si dispone di un tipo generico i cui costruttori sono esposti e si desidera utilizzare i wrapper newtype per indicare un tipo più semanticamente specifico. Utilizzando newtype s poi porta a chiamare siti come,

s1 = Specific1 $ General "Bob" 23 
s2 = Specific2 $ General "Joe" 19 

Dove il fatto che le rappresentazioni interne sono uguali tra le diverse newtypes specifici è trasparente.

L'approccio tag tipo va quasi sempre insieme con la rappresentazione costruttore nascondiglio,

data General2 a = General2 String Int 

e l'uso di costruttori intelligenti, portando ad una definizione del tipo di dati e di siti come chiamare,

mkSpecific1 "Bob" 23 

Parte il motivo è che si desidera un modo sintatticamente leggero di indicare quale tag si desidera. Se non hai fornito i costruttori intelligenti, il codice cliente spesso rileva annotazioni di tipo per restringere le cose, ad es.,

myValue = General2 String Int :: General2 Specific1 

Una volta che si adotta costruttori intelligenti, è possibile aggiungere facilmente ulteriore logica di validazione per la cattura di abusi del tag. Un aspetto interessante dell'approccio di tipo fantasma è che la corrispondenza del modello non viene affatto modificata per il codice interno che ha accesso alla rappresentazione.

internalFun :: General2 a -> General2 a -> Int 
internalFun (General2 _ age1) (General2 _ age2) = age1 + age2 

Naturalmente è possibile utilizzare i newtype s con costruttori intelligenti e una classe interna per l'accesso alla rappresentazione condivisa, ma penso che un punto di decisione chiave in questo spazio di progettazione è se si desidera mantenere i costruttori di rappresentazione a vista. Se la condivisione della rappresentazione deve essere trasparente e il codice client deve essere libero di utilizzare qualsiasi tag che desideri senza ulteriori validazioni, allora i wrapper newtype con GeneralizedNewtypeDeriving funzionano correttamente. Ma se si adottano costruttori intelligenti per lavorare con rappresentazioni opache, di solito preferisco i tipi fantasma.

+0

Se la memoria mi serve, 'dati Foo a = Foo a',' dati Foo ab = Foo a', e 'newtype Bar a = Bar (Foo a)' (con il primo 'Foo') dovrebbero essere tutti compilati allo stesso rappresentazione runtime, quindi trovare una differenza non banale nelle prestazioni sarebbe alquanto inaspettata. –

+1

@camccann La bellezza di ghc-core e Criterion è una prova empirica per integrare la memoria! :) Penso che la domanda sulle prestazioni abbia più a che fare con il fatto che le operazioni provenienti da una classe influiscano o meno sulle loro prestazioni rispetto alla rappresentazione runtime del valore stesso. Le funzioni polimorfiche vanno da '(Generale a, Generale b) => a -> b -> Int' a' Generale2 a -> Generale2 b -> Int'. – Anthony

3

C'è un altro approccio semplice.

data MyGenType = Foo | Bar 

op :: MyGenType -> MyGenType 
op x = ... 

op2 :: MyGenType -> MyGenType -> MyGenType 
op2 x y = ... 

newtype MySpecialType {unMySpecial :: MyGenType} 

inMySpecial f = MySpecialType . f . unMySpecial 
inMySpecial2 f x y = ... 

somefun = ... inMySpecial op x ... 
someOtherFun = ... inMySpecial2 op2 x y ... 

In alternativa,

newtype MySpecial a = MySpecial a 
instance Functor MySpecial where... 
instance Applicative MySpecial where... 

somefun = ... fmap op x ... 
someOtherFun = ... liftA2 op2 x y ... 

penso che questi approcci sono più belli se si desidera utilizzare il tipo generale "nudo" con qualsiasi frequenza, e solo qualche volta vuole etichettare essa. Se, d'altra parte, in genere si desidera utilizzarlo taggato, l'approccio del tipo fantasma esprime più direttamente ciò che si desidera.

+0

Davvero gradito quel tasto Maiusc oggi, eh? –

+0

Oops. Formattazione fissa – sclv

+1

btw, è possibile generare automaticamente inMySpecial1,2,3 ../ withMySpecial ecc con template haskell. vedi http://github.com/yairchu/peakachu/blob/master/src/Data/Newtype.hs – yairchu

1

Inserire una logica sufficiente in una classe di tipo per supportare le operazioni condivise che il caso d'uso desidera. Creare un tipo con la rappresentazione desiderata e creare un'istanza della classe del tipo per quel tipo. Quindi, per ogni caso d'uso, crea dei wrapper per esso con newtype e ricava la classe comune.

Questo presenta alcune insidie, a seconda della natura del tipo e del tipo di operazioni coinvolte.

Innanzitutto, costringe molte funzioni a essere inutilmente polimorfiche - anche se in pratica ogni istanza fa la stessa cosa per diversi wrapper, l'ipotesi del mondo aperto per le classi di tipi significa che il compilatore deve rendere conto della possibilità di altri le istanze. Mentre GHC è decisamente più intelligente del compilatore medio, più informazioni puoi dare e più è in grado di aiutarti.

In secondo luogo, questo può creare un collo di bottiglia per strutture dati più complicate. Qualsiasi funzione generica sui tipi avvolti sarà vincolata all'interfaccia presentata dalla classe type, quindi, a meno che quell'interfaccia sia esauriente sia in termini di espressività che di efficienza, si corre il rischio di algoritmi di hobbing che usano il tipo o alterano la classe del tipo ripetutamente quando trovi funzionalità mancanti.

D'altra parte, se il tipo spostato è già mantenuto astratto (vale a dire, non esporta costruttori) il problema del collo di bottiglia è irrilevante, quindi una classe di tipo potrebbe avere un senso. Altrimenti, probabilmente andrei con i tag di tipo fantasma (o forse con l'identità dell'identità Functor descritta da sclv).

Problemi correlati