2010-07-09 11 views
46

Ho una classe di caratteri MyClass e c'è una funzione in essa che produce un String. Voglio usare questo per implicare un'istanza di Show, in modo che io possa passare i tipi che implementano MyClass a show. Finora ho,Come scrivere, "se il tipo a, allora a è anche un'istanza di b per questa definizione."

class MyClass a where 
    someFunc :: a -> a 
    myShow :: a -> String 

instance MyClass a => Show a where 
    show a = myShow a 

che dà l'errore Constraint is no smaller than the instance head. ho anche provato,

class MyClass a where 
    someFunc :: a -> a 
    myShow :: a -> String 

instance Show (MyClass a) where 
    show a = myShow a 

che dà l'errore, Class MyClass' usati come type`.

Come posso esprimere correttamente questa relazione in Haskell? Grazie.

Vorrei aggiungere che desidero seguire questo con istanze specifiche di MyClass che emettono stringhe specifiche in base al loro tipo. Ad esempio,

data Foo = Foo 
data Bar = Bar 

instance MyClass Foo where 
    myShow a = "foo" 

instance MyClass Bar where 
    myShow a = "bar" 

main = do 
    print Foo 
    print Bar 
+1

FYI Ho letto questa domanda perché è dettagliata ma concisa, chiara e ben formata. Una domanda modello, e lo farei di nuovo se possibile. –

risposta

28

(Edit: lasciando il corpo per i posteri, ma saltare alla fine per la soluzione reale) "vincolo non è più piccola della testa istanza"

Nella dichiarazione instance MyClass a => Show a, esaminiamo l'errore Il vincolo è il vincolo della classe di tipo a sinistra di '=>', in questo caso MyClass a. La "testa di istanza" è tutto ciò che segue la classe per cui stai scrivendo un'istanza, in questo caso a (a destra di Show). Una delle regole di inferenza di tipo in GHC richiede che il vincolo abbia un numero minore di costruttori e variabili rispetto alla testa. Questo fa parte di quello che viene chiamato "Paterson Conditions". Questi esistono come garanzia che il controllo del tipo termina.

In questo caso, il vincolo è esattamente lo stesso della testina, ovvero a, quindi non ha superato questo test. È possibile rimuovere i controlli di condizione di Paterson abilitando UndecidableInstances, molto probabilmente con il pragma {-# LANGUAGE UndecidableInstances #-}.

In questo caso, si sta essenzialmente utilizzando la classe MyClass come sinonimo di classe per la classe Show. La creazione di sinonimi di classe come questo è uno degli usi canonici per l'estensione UndecidableInstances, quindi puoi tranquillamente usarlo qui.

'Indecibile' significa che GHC non può provare che il controllo della digitazione terminerà. Sebbene sembri pericoloso, il peggio che può capitare dall'abilitazione di UndecidableInstances è che il compilatore eseguirà un loop, eventualmente terminando dopo aver esaurito lo stack. Se si compila, allora ovviamente il typechecking è terminato, quindi non ci sono problemi. L'estensione pericolosa è IncoherentInstances, che è così male come sembra.

Edit: un altro problema reso possibile da questo approccio deriva da questa situazione:

instance MyClass a => Show a where 

data MyFoo = MyFoo ... deriving (Show) 

instance MyClass MyFoo where 

Ora ci sono due istanze di Visualizza per MyFoo, quello dalla clausola derivanti e quello per MyClass istanze. Il compilatore non può decidere quale usare, quindi si scaricherà con un messaggio di errore. Se stai cercando di rendere le istanze di tipi MyClass che non hai ancora in possesso delle istanze Show, dovrai utilizzare newtypes per nascondere le istanze Show già esistenti. Anche i tipi senza MyClass casi sarà ancora conflitto, perché la definizione instance MyClass => Show a perché la definizione fornisce in realtà un implementazione per tutte le possibili a (l'assegno contesto entra in gioco più tardi, la sua non è coinvolto con la selezione esempio)

Ecco, questo è il messaggio di errore e come UndecidableInstances lo fa andare via. Sfortunatamente è un sacco di problemi da usare nel codice reale, per le ragioni spiegate da Edward Kmett. L'impulso originale era di evitare di specificare un vincolo Show quando esiste già un vincolo MyClass. Detto questo, quello che vorrei fare è utilizzare myShow da MyClass anziché show. Non è necessario il vincolo Show.

+0

Grazie, funziona davvero se abilito UndecidableInstances. Strano. Si tratta di un abuso del sistema di classi di tipografia? Stavo principalmente cercando di evitare di dover specificare il vincolo "Mostra" per tutte le funzioni che richiedono "MyClass" e anche bisogno di mostrare. Dal momento che tutte le istanze di MyClass dovrebbero avere una stringa associata a loro, ho pensato che derivare automaticamente Show sarebbe un modo. Tuttavia, sembra meglio evitare il problema specificando esplicitamente Mostra dove necessario, invece di abilitare le estensioni del linguaggio esoterico. Cosa ne pensi? – Steve

+3

Vorrei usare myShow invece di mostrare in tutte le funzioni quindi non è necessario il vincolo Mostra. Nelle definizioni dell'istanza puoi quindi scrivere 'myShow = show' se è appropriato. Eviterei di scrivere 'istanza MyClass a => Mostra a' a causa del problema descritto nella modifica. La soluzione di Dave Hinton è migliore di UndecidableInstances, ma non penso che non dovresti aggiungere un vincolo della superclasse solo per comodità. –

+2

Ricorda che questo hack con queste estensioni funziona solo in modo ragionevolmente sicuro all'interno di un singolo modulo. Non puoi nemmeno usare le esportazioni da questo modulo, anche se non esporti MyClass, in nessun altro modulo che abbia mai un modulo che dipende in via transitiva da esso e che voglia mai mostrare qualcosa che non sia un'istanza di MyClass. –

2

È possibile compilarlo, ma non con Haskell 98, è necessario abilitare alcune estensioni del linguaggio:

{-# LANGUAGE FlexibleInstances #-} 
{-# LANGUAGE UndecidableInstances #-} 
-- at the top of your file 

casi flessibili è lì per consentire contesto in dichiarazione di istanza. Non so davvero il significato di UndecidableInstances, ma eviterei il più possibile.

+0

Supponevo che il fatto che GHC ritenga che sia indecidibile è probabilmente un problema. Se qualcuno potesse spiegare perché è indecidibile sarebbe fantastico! – Steve

+3

Se si abilita UndecidableInstances, FlexibleInstances è superfluo. –

5

penso che sarebbe meglio farlo il contrario:

class Show a => MyClass a where 
    someFunc :: a -> a 

myShow :: MyClass a => a -> String 
myShow = show 
+0

... eccetto che non permetterà all'OP di dichiarare una versione 'show' personalizzata. Inoltre, non consentirà una dichiarazione di 'MyClass' per un tipo che non è (ancora) un'istanza Show. – BMeph

+4

La cosa da capire qui è che quando dici show = myShow, hai già perso la battaglia di avere uno show personalizzato poiché un tipo non può avere due diverse funzioni show, e un tipo che implementa MyClass deve essere un'istanza di Show (chiamandolo myShow instaid of show non fa differenza.) Cioè se voglio che T sia un'istanza di MyClass, devo implementare myShow, alla fine che risulta essere la sua funzione show come un'istanza di Show. Confrontalo alla prima implementazione di Show e quindi implementando MyClass e vedi che l'unica differenza è il nome della funzione. – HaskellElephant

+1

Mi piace questa soluzione perché il modo in cui l'OP presenta il suo problema, 'myShow' è equivalente a' show' per tutte le istanze di 'MyClass' perché' MyClass' implicherebbe 'Show' (secondo il desiderio di OP). Tuttavia, in questo caso non avresti bisogno della funzione 'myShow'. Basta usare 'show'. –

53

Vorrei dissentire energicamente con le soluzioni rotti poste finora.

A causa del modo in cui la risoluzione dell'istanza funziona, questa è un'istanza molto pericolosa da avere in giro!

La risoluzione di istanza procede efficacemente sulla corrispondenza del modello sul lato destro di => di ciascuna istanza, completamente senza riguardo a ciò che si trova sulla sinistra dello =>.

Quando nessuna di queste istanze si sovrappone, questa è una cosa meravigliosa. Tuttavia, quello che stai dicendo qui è "Ecco una regola da utilizzare per OGNI Mostra istanza. Quando viene richiesta un'istanza di presentazione per qualsiasi tipo, è necessaria un'istanza di MyClass, quindi vai a prenderla, e qui è l'implemento." - una volta che il compilatore si è impegnato a scegliere di utilizzare l'istanza, (solo in virtù del fatto che "a" si unifica con tutto) non ha alcuna possibilità di ricorrere e utilizzare altre istanze!

Se si attiva {-# LANGUAGE OverlappingInstances, IncoherentInstances #-}, ecc. Per renderlo compilato, si otterranno errori non così sottili quando si va a scrivere moduli che importano il modulo che fornisce questa definizione e devono utilizzare qualsiasi altra istanza Show. Alla fine sarai in grado di compilare questo codice con abbastanza estensioni, ma purtroppo non farà ciò che pensi che dovrebbe fare!

Se ci pensate dato:

instance MyClass a => Show a where 
    show = myShow 

instance HisClass a => Show a where 
    show = hisShow 

che dovrebbe prendere il compilatore?

Il modulo può solo definire uno di questi, ma il codice utente finale importa un mucchio di moduli, non solo il tuo.Inoltre, se un altro modulo definisce

instance Show HisDataTypeThatHasNeverHeardOfMyClass 

il compilatore sarebbe andato bene nel suo pieno diritto di ignorare la sua istanza e provare a utilizzare il vostro.

La risposta giusta, purtroppo, è fare due cose.

Per ogni individuale istanza di MyClass è possibile definire una corrispondente istanza di Show con la definizione molto meccanico

instance MyClass Foo where ... 

instance Show Foo where 
    show = myShow 

Questo è abbastanza spiacevole, ma funziona bene quando ci sono solo pochi esempi di MyClass sotto considerazione.

Quando si dispone di un numero elevato di istanze, è necessario definire il modo per evitare la duplicazione del codice (per quando la classe è notevolmente più complicata di mostrare).

newtype WrappedMyClass a = WrapMyClass { unwrapMyClass :: a } 

instance MyClass a => Show (WrappedMyClass a) where 
    show (WrapMyClass a) = myShow a 

Ciò fornisce il newtype come un veicolo per esempio la spedizione. e poi

instance Foo a => Show (WrappedFoo a) where ... 
instance Bar a => Show (WrappedBar a) where ... 

è univoca, perché il tipo 'pattern' per WrappedFoo a e WrappedBar a sono disgiunti.

Nel pacchetto base esistono diversi esempi di questo idioma.

In Control.Applicativo ci sono definizioni per WrappedMonad e WrappedArrow proprio per questo motivo.

Idealmente si sarebbe in grado di dire:

instance Monad t => Applicative t where 
    pure = return 
    (<*>) = ap 

ma in modo efficace ciò che questo caso sta dicendo è che ogni applicativo deve essere derivato trovando prima un'istanza per la Monade, e poi l'invio ad esso. Così, mentre avrebbe l'intenzione di dire che ogni Monade è Applicativa (dal modo in cui si legge l'implicazione =>) ciò che in realtà dice è che ogni Applicativo è una Monade, perché avere un'istanza di testa 't' corrisponde a qualsiasi tipo. In molti modi, la sintassi per le definizioni di 'istanza' e 'classe' è al contrario.

+1

Hah, sono stato così preso dalla spiegazione del messaggio di errore e da ciò che UndecidableInstances fa che ho completamente dimenticato di inserire nella mia soluzione reale (tranne in un piccolo commento)! Ho modificato la mia risposta per includerla. –

+3

Grazie! Come sono sicuro che tu possa dire dalla mia domanda, sto ancora imparando esattamente quali tipi di tipografia "sono", e questo aiuta enormemente. Per quanto non mi piacciano i "modelli di progettazione", sarebbe bello vedere un libro che dettaglia le soluzioni dei libri di cucina alle situazioni comuni nei programmi Haskell, incluso il "linguaggio" sopra come lo chiami. – Steve

+0

A proposito, è sicuro di dire, "una classe di caratteri non può (in tutta sicurezza) ricavare automaticamente (inferire) un'altra classe, può solo richiedere (essere vincolata da) una?" – Steve

1

Come Ed Kmett sottolineato, questo non è possibile a tutti per il vostro caso. Se invece si ha accesso alla classe si desidera fornire un'istanza predefinita per, è possibile ridurre il boilerplate al minimo con un'implementazione di default e vincolare il tipo di ingresso con la firma predefinita è necessario:

{-# LANGUAGE DefaultSignatures #-} 

class MyClass a where 
    someFunc :: a -> Int 

class MyShow a where 
    myShow :: a -> String 
    default myShow :: MyClass a => a -> String 
    myShow = show . someFunc 

instance MyClass Int where 
    someFunc i = i 

instance MyShow Int 

main = putStrLn (myShow 5) 

Si noti che l'unico vero boilerplate (beh, a parte l'intero esempio) ridotto a instance MyShow Int.

Vedere aeson s ToJSON per un esempio più realistico.

Problemi correlati