7

Sto cercando di rappresentare espressioni con famiglie di tipi, ma non riesco a capire come scrivere i vincoli che voglio, e sto iniziando a sentire come se non fosse possibile. Ecco il mio codice:Haskell tipo istanza famiglia con vincoli di tipo

class Evaluable c where 
    type Return c :: * 
    evaluate :: c -> Return c 

data Negate n = Negate n 

instance (Evaluable n, Return n ~ Int) => Evaluable (Negate n) where 
    type Return (Negate n) = Return n 
    evaluate (Negate n) = negate (evaluate n) 

Il tutto compila bene, ma non esprime esattamente quello che voglio. Nei vincoli dell'istanza Negate di Evaluable, dico che il tipo restituito dell'espressione all'interno di Negate deve essere un Int (con Return n ~ Int) in modo che sia possibile chiamare negare su di esso, ma che è troppo restrittivo. Il tipo restituito deve essere solo un'istanza della classe di tipo Num che ha la funzione negate. In questo modo Double s, Integer s, o qualsiasi altra istanza di Num potrebbero anche essere negati e non solo Int s. Ma non posso solo scrivere

Return n ~ Num 

, invece, perché Num è una classe tipo e Return n è un tipo. Anche non posso mettere

Num (Return n) 

invece perché Return n è un tipo non una variabile di tipo.

È quello che sto cercando di fare anche con Haskell? In caso contrario, dovrebbe essere, o sto fraintendendo qualche teoria dietro di esso? Mi sento come se Java potesse aggiungere un vincolo come questo. Fammi sapere se questa domanda potrebbe essere più chiara.

Modifica: Grazie ragazzi, le risposte stanno aiutando e stanno arrivando a quello che sospettavo. Sembra che il type checker non sia in grado di gestire ciò che mi piacerebbe fare senza UndecidableInstances, quindi la mia domanda è, è ciò che vorrei esprimere davvero indecidibile? È per il compilatore Haskell, ma è in generale? cioè potrebbe esistere un vincolo che significa "verificare che Return n sia un'istanza di Num" che è decidibile per un controllo di tipo più avanzato?

+0

btw, GHC non ha mai consigliato un'estensione di lingua, come ad es. 'FlexibleContexts' o qualcos'altro, durante il processo di prova ed errore? perché sono abbastanza sicuro di averlo fatto - solo una nota a margine sul _ "è anche possibile con Haskell" _ bit. –

risposta

6

In realtà, si può fare esattamente ciò che lei ha citato:

{-# LANGUAGE TypeFamilies, FlexibleContexts, UndecidableInstances #-} 

class Evaluable c where 
    type Return c :: * 
    evaluate :: c -> Return c 

data Negate n = Negate n 

instance (Evaluable n, Num (Return n)) => Evaluable (Negate n) where 
    type Return (Negate n) = Return n 
    evaluate (Negate n) = negate (evaluate n) 

Return n è certamente un tipo, che può essere un'istanza di una classe proprio come Int can. La tua confusione potrebbe riguardare ciò che può essere l'argomento di un vincolo. La risposta è "qualsiasi cosa abbia il tipo corretto". Il tipo di Int è *, come il tipo di Return n. Num ha tipo * -> Constraint, quindi qualsiasi cosa di tipo * può essere la sua argomentazione. È perfettamente legale (anche se vuoto) scrivere Num Int come vincolo, nello stesso modo in cui è legale Num (a :: *).

+0

Giusto, avrei dovuto spiegare che ho già esaminato questo aspetto, ma le estensioni FlexibleContexts e UndecidableInstances mi rendono nervoso. Mi sento come se potessero tornare a mordermi alla fine quando aggiungo altre istanze, a meno che non mi sbagli. – user3773003

+4

Per quanto ne so, 'FlexibleContexts' è completamente sicuro e semplicemente consente le variabili di tipo non come parametri di tipo. 'UndecidableInstances' può causare la non-terminazione durante la compilazione se le istanze formano un ciclo (poiché il compilatore non può controllarlo). Per maggiori informazioni, vedere [questa risposta] (http://stackoverflow.com/a/5017549/925978). – crockeea

+7

@ user3773003: Voglio dire qualcosa di più forte di Eric: non penso che sia ragionevole usare 'TypeFamilies' senza' FlexibleContexts', e probabilmente non senza 'UndecidableInstances'! Le regole di contesto non flessibili sono state progettate per una lingua senza famiglie di caratteri; non sorprende che abbiano bisogno di un'estensione. E poiché le famiglie di tipi sono funzioni a livello di codice, non sorprende che il controllo di terminazione (di tipo livello) possa essere d'intralcio, soprattutto perché non è molto intelligente; non abbiamo un controllo di terminazione a livello di valore, quindi 'UndecidableInstances' ci offre un livello di tipo espressivo simile. –

2

Per completare la risposta di Eric, Mi permetto di suggerire una possibile alternativa: usando una dipendenza funzionale, invece di una famiglia tipo:

class EvaluableFD r c | c -> r where 
    evaluate :: c -> r 

data Negate n = Negate n 

instance (EvaluableFD r n, Num r) => EvaluableFD r (Negate n) where 
    evaluate (Negate n) = negate (evaluate n) 

Questo lo rende un po 'più facile per parlare del tipo di risultato, credo.Per esempio, è possibile scrivere

foo :: EvaluableFD Int a => Negate a -> Int 
foo x = evaluate x + 12 

È inoltre possibile utilizzare ConstraintKinds di applicare questa parte (che è il motivo per cui ho messo gli argomenti in questo ordine buffo):

type GivesInt = EvaluableFD Int 

Si potrebbe fare questo con la tua classe, ma sarebbe più fastidioso:

type GivesInt x = (Evaluable x, Result x ~ Int) 
+0

Sì, ancora qualcosa che avrei dovuto menzionare che ho provato nella domanda originale. Anche questo ha bisogno di UndecidableInstances e anche di FlexibleInstances che vorrei evitare. – user3773003

+0

@ user3773003, non sono troppo ottimista per ottenere quello che vuoi senza queste estensioni. Potrei mancare qualcosa, ovviamente. – dfeuer

+0

@ user3773003 Non c'è letteralmente alcun svantaggio di 'FlexibleInstances' e' UndecidableInstances' è anche innocuo. –