2013-07-25 9 views
16

È possibile creare un typeclass che non può più ammettere nuovi membri (forse utilizzando i limiti del modulo)? Posso rifiutare di esportare una funzione necessaria per una definizione completa dell'istanza, ma questo si traduce solo in un errore di runtime se qualcuno produce un'istanza non valida. Posso fare un errore in fase di compilazione?Classi di tipo chiuso

+2

Queste risposte sono tutto ciò che gran-grazie everone! –

risposta

13

Credo che la risposta sia un sì qualificato, a seconda di cosa si sta cercando di ottenere.

È possibile astenersi dall'esportare il nome classe del tipo dal modulo di interfaccia , mentre si continuano ad esportare i nomi delle funzioni di classe del tipo. Quindi nessuno può fare un'istanza della classe perché nessuno può nominarla!

Esempio:

module Foo (
    foo, 
    bar 
) where 

class SecretClass a where 
    foo :: a 
    bar :: a -> a -> a 

instance SecretClass Int where 
    foo = 3 
    bar = (+) 

il lato negativo è che nessuno può scrivere un tipo con la classe come un vincolo sia. Questo non è interamente impedire alle persone di scrivere funzioni che avrebbero un tale tipo, perché il compilatore sarà ancora in grado di inferire il tipo. Ma sarebbe molto fastidioso.

È possibile attenuare lo svantaggio fornendo un'altra classe di tipo vuota, con la classe "chiusa" come una super classe. Si crea un'istanza della sottoclasse in ogni istanza della classe originale e si esporta la sottoclasse (insieme a tutte le funzioni della classe tipo), ma non la super classe. (Per chiarezza dovresti probabilmente usare la classe "pubblica" piuttosto che quella "segreta" in tutti i tipi che esponi, ma credo che funzioni in entrambi i casi).

Esempio:

{-# LANGUAGE FlexibleInstances, UndecidableInstances #-} 

module Foo ( 
    PublicClass, 
    foo, 
    bar 
) where 

class SecretClass a where 
    foo :: a 
    bar :: a -> a -> a 

class SecretClass a => PublicClass a 

instance SecretClass Int where 
    foo = 3 
    bar = (+) 

instance SecretClass a => PublicClass a 

si può fare senza le estensioni, se siete disposti a dichiarare manualmente un'istanza di PublicClass per ogni istanza di SecretClass.

Ora codice del client può usare PublicClass scrivere vincoli tipo di classe, ma ogni istanza di PublicClass richiede un'istanza SecretClass per lo stesso tipo, e senza la possibilità di dichiarare una nuova istanza di SecretClass nessuno può apportare più istanze tipi di PublicClass .

Che cosa tutto questo non ottenere è la possibilità per il compilatore di considerare la classe come "chiusa". Continuerà a lamentarsi di variabili di tipo ambigue che potrebbero essere risolte selezionando l'unica istanza visibile di "chiuso".


parere puro: di solito è una buona idea avere un modulo interno separato con un nome spaventoso che esporta tutto in modo che si può ottenere a esso per il test/debug, con un modulo di interfaccia che importa il modulo interno e esporta solo le cose che vuoi esportare.

Immagino che con estensioni qualcuno possa dichiarare una nuova istanza sovrapposta. Per esempio. se hai fornito un'istanza per [a], qualcuno potrebbe dichiarare una nuova istanza di PublicClass per [Int], che farebbe il dorso sull'istanza di SecretClass per [a].Ma dato che PublicClass non ha funzioni e non possono scrivere un'istanza di SecretClass non riesco a vedere che si possa fare molto con questo.

+0

Tuttavia, chiunque può ancora creare istanze di 'PublicClass' e sebbene io non sappia cosa stava cercando di ottenere, non posso pensare a uno scenario in cui ciò non avrebbe le stesse implicazioni dell'esportazione di' SecretClass' nel primo posto. Puoi? – firefrorefiddle

+1

@MikeHartl No, non è possibile creare istanze di 'PublicClass' per tipi che non sono già istanze di' SecretClass'. Si ottiene un errore come 'Nessuna istanza per (Foo.SecretClass Bool) derivante dalle superclassi di una dichiarazione di istanza Possibile correzione: aggiungere una dichiarazione di istanza per (Foo.SecretClass Bool)'. E non puoi aggiungere quell'istanza, perché 'SecretClass' non si trova in nessun punto fuori dal modulo. – Ben

+0

Ah! Errore mio. L'ho scambiato con 'instance (SecretClass a) => PublicClass a'. – firefrorefiddle

18

Dal GHC 7.8.1, closed type families può essere dichiarata, e credo che con l'aiuto di loro, e ConstraintKinds, si può fare questo:

type family SecretClass (a :: *) :: Constraint where 
    SecretClass Int =() 

SecretClass a forma un vincolo, equivalente a una classe tipo, e poiché la famiglia non può essere estesa da nessuno, non è possibile definire altre istanze della "classe".

(Questo in realtà è solo speculazione, visto che non posso provarlo, ma il codice in this interesting link fa apparire come avrebbe funzionato.)

+1

Idea pulita, grazie. –

+1

Entrambi i link al documento di Patrick Bahr sono 404. Prova [questo link] (http://bahr.io/pubs/files/serrano15haskell-paper.pdf), dal sito di Bahr. –

7

Si potrebbe refactoring il typeclass in una dichiarazione di dati (sintassi record di utilizzo) che contiene tutte le funzioni possedute dal tuo typeclass. Un elenco limitato di istanze fisso suona come se non avessi bisogno di una classe in ogni caso.

Questo è, naturalmente, essenzialmente ciò che il compilatore sta facendo behibd le scene con la classe comunque.

Ciò consentirebbe di esportare l'elenco di istanze come funzioni del tipo di dati, ed è possibile esportarle ma non i costruttori per il tipo di dati. Allo stesso modo, è possibile limitare l'esportazione delle funzioni di accesso e esportare semplicemente l'interfaccia che si desidera effettivamente.

Questo funziona correttamente perché i tipi di dati non sono soggetti all'ipotesi di open world di attraversamento del confine tra modulo e tipo.

A volte aggiungere la complessità del sistema rende solo le cose più difficili.

11

È possibile codificare classi di tipi chiuse tramite famiglie di tipi chiusi, che possono essere codificati essenzialmente come famiglie di tipi associate a turno. La chiave di questa soluzione è che le istanze di una famiglia di tipi associata si trovano all'interno di un'istanza di classe di tipo e può esistere solo un'istanza di classe di tipo per ogni tipo monomorfico.

Si noti che questo approccio è indipendente dal sistema del modulo. Invece di fare affidamento sui limiti del modulo, forniamo un elenco esplicito di quali istanze sono legali. Ciò significa, da un lato, che le istanze legali possono essere distribuite su più moduli o anche pacchetti e, dall'altro, che non possiamo fornire istanze illegali anche nello stesso modulo.

Per questa risposta, presumo che vogliamo chiudere la seguente classe in modo che possa essere istanziata solo per il tipo Int e Integer, ma non per altri tipi:

-- not yet closed 
class Example a where 
    method :: a -> a 

In primo luogo, abbiamo bisogno di un piccolo framework per codificare famiglie di tipi chiusi come famiglie di tipi associati.

{-# LANGUAGE TypeFamilies, EmptyDataDecls #-} 

class Closed c where 
    type Instance c a 

Il parametro c sta per il nome della famiglia tipo e il parametro a è l'indice della famiglia tipo. L'istanza di famiglia di c per a è codificata come Instance c a. Poiché c è anche un parametro di classe, tutte le istanze di famiglia di c devono essere fornite insieme, in una singola dichiarazione di istanza di classe.

Ora, usiamo questo quadro per definire un tipo chiuso famiglia MemberOfExample per codificare che Int e Integer sono Ok, e tutti gli altri tipi non lo sono.

data MemberOfExample 
data Ok 

instance Closed MemberOfExample where 
    type Instance MemberOfExample Int = Ok 
    type Instance MemberOfExample Integer = Ok 

Infine, usiamo questo tipo di famiglia chiuso in contraint superclasse della nostra Example.

class Instance MemberOfExample a ~ Ok => Example a where 
    method :: a -> a 

Possiamo definire le istanze valide per Int e Integer come al solito.

instance Example Int where 
    method x = x + 1 

instance Example Integer where 
    method x = x + 1 

Ma non possiamo definire i casi non validi per altri tipi di Int e Integer.

-- GHC error: Couldn't match type `Instance MemberOfExample Float' with `Ok' 
instance Example Float where 
    method x = x + 1 

E non possiamo neanche estendere l'insieme di tipi validi.

-- GHC error: Duplicate instance declarations 
instance Closed MemberOfExample where 
    type Instance MemberOfExample Float = Ok 

-- GHC error: Associated type `Instance' must be inside a class instance 
type instance Instance MemberOfExample Float = Ok 

Purtroppo, siamo in grado di scrivere il seguente esempio fasulla:

-- Unfortunately accepted 
instance Instance MemberOfExample Float ~ Ok => Example Float where 
    method x = x + 1 

Ma dal momento che non saremo mai in grado di scaricare il vincolo di uguaglianza, non credo che possiamo mai usarlo per qualsiasi cosa. Ad esempio, viene respinto il seguente:

-- Couldn't match type `Instance MemberOfExample Float' with `Ok' 
test = method (pi :: Float) 
+0

Questo sembra essere molto simile alla risposta di @ Ben. Puoi spiegare come differiscono? –

+0

@tel: la differenza più importante sembra essere che questo approccio è indipendente dal sistema del modulo. Ho aggiunto un paragrafo alla risposta per farlo notare. – Toxaris

1

Quando tutto si è interessati a che si dispone di un set enumertated di istanze, quindi questo trucco potrebbe aiutare:

class (Elem t '[Int, Integer, Bool] ~ True) => Closed t where 

type family Elem (t :: k) (ts :: [k]) :: Bool where 
    Elem a '[] = False 
    Elem a (a ': as) = True 
    Elem a (b ': bs) = Elem a bs 

instance Closed Int 
instance Closed Integer 
-- instance Closed Float -- ERROR