2016-12-16 5 views
4

J'ai exploré l'utilisation de plusieurs enveloppes newtype dans mon code pour créer des types plus distincts. Je fais aussi beaucoup de sérialisation bon marché en utilisant Read/Show, particulièrement comme une simple forme de fichier de configuration fortement typé. Je suis tombé sur aujourd'hui:Instance de classe de type non utilisée lors de la dérivation contenant une structure de données

L'exemple commence comme ça, et je définir simple newtype à enrouler autour de Int, avec un champ nommé pour déballer:

module Main where 

import Debug.Trace (trace) 
import Text.Read (readEither) 


newtype Bar = Bar { unBar :: Int } 
    deriving Show 

exemple sur mesure pour lire un de ces de une syntaxe Int simple. L'idée ici est que ce serait génial de pouvoir mettre "42" dans un fichier de configuration au lieu de "Bar {unBar = 42}"

Cette instance a aussi une trace "logging" donc on peut voir quand cette instance est vraiment utilisé lors de l'observation du problème.

instance Read Bar where 
    readsPrec _ s = [(Bar i, "")] 
     where i = read (trace ("[debug \"" ++ s ++ "\"]") s) 

Maintenant un autre type contenant une barre. Celui-ci va juste auto-dériver Read.

data Foo = Foo { bar :: Bar } 
    deriving (Read, Show) 


main :: IO() 
main = do 

désérialisation le type de bar fonctionne seul bien et utilise l'instance Lire ci-dessus

print $ ((readEither "42") :: Either String Bar) 
    putStrLn "" 

Mais pour une raison quelconque Foo, contenant une barre, et dérivé automatiquement en lecture, ne fore vers le bas et ramasser Les instances de Bar! (Notez que le message de débogage de trace ne soit affiché)

print $ ((readEither "Foo { bar = 42 }") :: Either String Foo) 
    putStrLn "" 

Alors ok, pourquoi la valeur par défaut Afficher sous forme de barre, doit correspondre à la valeur par défaut Lire droit?

print $ ((readEither "Foo { bar = Bar { unBar = 42 } }") :: Either String Foo) 

Non! Ne fonctionne pas non plus !! Encore une fois, pas de message de débogage.

est ici la sortie d'exécution:

$ stack exec readbug 
    [debug "42"] 
    Right (Bar {unBar = 42}) 

    Left "Prelude.read: no parse" 

    Left "Prelude.read: no parse" 

Cela semble buggy pour moi, mais j'aimerais entendre que je le fais mal.

Un exemple complet du code ci-dessus est disponible. Voir le fichier src/Main.lhs dans un test project on darcshub

+3

C'est une très bonne question. J'adore la facilité avec laquelle vous avez commencé à déboguer votre code. J'espère que ma réponse est utile pour identifier le problème particulier que vous rencontrez. Cela mis à part, je ne recommanderais jamais d'utiliser 'Read' pour autre chose que le débogage - et ensuite assurez-vous que' read. show = id'. Je voudrais mettre ma config dans un JSON (et utiliser 'aeson' pour encoder/décoder), ou (si vous insistez sur un analyseur personnalisé) utiliser quelque chose comme' attoparsec' ou 'megaparsec'. 'Read' est un analyseur phénoménalement inefficace, car il est prêt à faire marche arrière n'importe où. – Alec

+2

Vous avez tort: ​​l'instance dérivée de 'Foo' * est * en utilisant l'instance' Read' de 'Bar' que vous avez écrite! C'est juste que l'instance 'Foo' échoue avant qu'elle ne dérange pour forcer la valeur de' Bar' (donc ne force jamais le thunk avec la 'trace' dedans), parce que' Bar' signale incorrectement qu'elle a consommé toutes les entrées restantes et le lecteur 'Foo' ne voit donc pas le'} 'dont il a besoin pour réussir. –

+0

@Alec Je n'avais pas envisagé d'utiliser JSON pour les configs. Conserve la structure typographique et hiérarchique. Et puis vous vous retrouvez avec un fichier de configuration qui est utilisable par d'autres langages/systèmes. Je vais explorer cela avec newtypes un peu maintenant. Merci! – dino

Répondre

4

Le problème est dans Read. readsPrec doit considérer la possibilité qu'il pourrait voir plus de choses après le Bar. Quoting the Prelude:

readsPrec d s tente d'analyser une valeur de l'avant de la chaîne, retournant une liste de paires (<parsed value>, <remaining string>). S'il n'y a pas d'analyse réussie, la liste retournée est vide.

Dans votre cas, vous voulez:

instance Read Bar where 
    readsPrec d s = [ (Bar i, s') | (i, s') <- readsPrec d tracedS ] 
     where tracedS = trace ("[debug \"" ++ s ++ "\"]") s 

Ensuite, les travaux suivants:

ghci> print $ ((readEither "Foo { bar = 42 }") :: Either String Foo) 
[debug " 42 }"] 
Right (Foo {bar = Bar {unBar = 42}}) 

Votre autre problème, à savoir:

Alors ok, que diriez-vous le formulaire Show par défaut pour Bar, devrait correspondre à la lecture par défaut?

print $ ((readEither "Foo { bar = Bar { unBar = 42 } }") :: Either String Foo) 

est votre faute: vous avez défini une instance Read pour Bar telle que read . show n'est pas une opération d'identité. Lorsque Foo dérive Read, il utilise Bar s Read instance (il n'essaie pas de régénérer le code que Bar aurait généré si vous y aviez dérivé Read).

+1

Je sais que c'est une sorte de tangente, mais je ne peux m'empêcher de le suggérer: 'Read Bar où readsPrec = coerce (readsPrec @Int)' –

+0

@Alec Ah! Je n'ai pas réalisé cette différence entre 'read' et' readsPrec' mais il est beaucoup plus logique d'utiliser la même fonction. Je vous remercie. – dino

+0

@DanielWagner Ooh, j'aime bien la lacune de ça. Je ne connaissais pas Data.Coerce. Je me demande si c'est plus efficace. Nécessite également '-XTypeApplications' Merci! – dino