2017-10-18 12 views
2

J'ai des types de données avec de nombreux champs qui, s'ils ne sont pas spécifiés manuellement par un fichier de configuration JSON, doivent être définis au hasard. J'utilise Aeson pour analyser le fichier de configuration. Quelle est la meilleure façon de procéder?Comment utiliser les analyseurs d'Aeson avec IO

Actuellement, je mets des valeurs égales à une valeur impossible, puis je vérifie plus tard la valeur à éditer.

data Example = Example { a :: Int, b :: Int } 
default = Example 1 2 
instance FromJSON Example where 
    parseJSON = withObject "Example" $ \v -> Example 
     <$> (v .: "a" <|> return (a default)) 
     <*> (v .: "b" <|> return (b default)) 

initExample :: Range -> Example -> IO Example 
initExample range (Example x y) = do 
    a' <- if x == (a default) then randomIO range else return x 
    b' <- if y == (b default) then randomIO range else return y 
    return $ Example a' b' 

Ce que je voudrais est quelque chose le long des lignes de:

parseJSON = withObject "Example" $ \v -> Example 
     <$> (v .: "a" <|> return (randomRIO (1,10)) 

Est-il possible de définir Parsers dans le IO Monade ou d'un fil le long de certains générateur aléatoire, en utilisant idéalement Aeson?

Répondre

4

Eh bien, je ne sais pas si c'est une bonne idée, et jongler avec la couche IO supplémentaire va certainement devenir frustrant que diable pour les développements plus importants, mais les contrôles de type suivants:

{-# LANGUAGE FlexibleContexts #-} 
{-# LANGUAGE FlexibleInstances #-} 
{-# LANGUAGE OverloadedStrings #-} 
import Control.Applicative 
import Data.Aeson 
import System.Random 

data Example = Example { a :: Int, b :: Int } deriving (Eq, Ord, Read, Show) 

instance FromJSON (IO Example) where 
    parseJSON = withObject "Example" $ \v -> liftA2 Example 
     <$> ((pure <$> (v .: "a")) <|> pure (randomRIO (3, 4))) 
     <*> ((pure <$> (v .: "b")) <|> pure (randomRIO (5, 6))) 

Dans chacun les deux dernières lignes, la première pure est Int -> IO Int, et la seconde est IO Int -> Parser (IO Int). En ghci:

> sequence (decode "{}") :: IO (Maybe Example) 
Just (Example {a = 4, b = 6}) 
+0

Comme mentionné par @danidiaz, il utilise essentiellement les instances 'Applicative' et' Alternative' pour 'Compose Parser IO'. On pourrait soit utiliser directement ce type, soit l'utiliser comme guide supplémentaire pour écrire un tel code dans des développements plus importants. –

2

Je ne sais pas d'une bonne stratégie pour arriver là où vous voulez être depuis la monade parseJSON n'est pas un transformateur ou basé sur IO. Ce que vous pouvez faire plus facilement est de décoder en un type puis de traduire dans le second comme fait dans une question précédente 'Give a default value for fields not available in json using aeson'. Comme les structures volumineuses peuvent être difficiles à reproduire, vous pouvez paramétrer la structure et l'instancier avec IO Int ou Int. Par exemple, supposons que vous vouliez champ a du fil, mais b comme au hasard de la monade IO:

{-# LANGUAGE FlexibleInstances #-} 
{-# LANGUAGE OverloadedStrings #-} 
{-# LANGUAGE DeriveFoldable #-} 
{-# LANGUAGE DeriveTraversable #-} 

import Data.Aeson 
import System.Random 
import Data.ByteString.Lazy (ByteString) 

data Example' a = 
     Example { a :: Int 
       , b :: a 
       } deriving (Show,Functor,Foldable,Traversable) 

type Partial = Example' (IO Int) 

type Example = Example' Int 

instance FromJSON Partial where 
    parseJSON (Object o) = 
     Example <$> o .: "a" 
       <*> pure (randomRIO (1,10)) 

loadExample :: Partial -> IO Example 
loadExample = mapM id 

parseExample :: ByteString -> IO (Maybe Example) 
parseExample = maybe (pure Nothing) (fmap Just . loadExample) . decode 

Remarquez comment loadExample utilise notre traverse exemple pour exécuter les actions IO à l'intérieur de la structure. Voici un exemple d'utilisation:

Main> parseExample "{ \"a\" : 1111 }" 
Just (Example {a = 1111, b = 5}) 

avancée

Si vous aviez plus d'un type de champ pour lequel vous vouliez une action IO vous pouvez soit

  1. Faire un type de données pour chacun d'eux. Au lieu de b étant le type IO Int vous pourriez le faire IO MyComplexRecord. C'est la solution facile.

  2. La solution la plus complexe et amusante consiste à utiliser un paramètre de type supérieur.

Pour l'option 2, tenez compte:

data Example' f = Example { a :: Int 
          , b :: f Int 
          , c :: f String } 

Vous pouvez ensuite utiliser Proxy et Control.Monad.Identity au lieu des valeurs comme IO Int et Int utilisé précédemment.Vous devrez écrire votre propre parcours puisque vous ne pouvez pas dériver Traverse pour cette classe (ce qui nous donne le mapM utilisé ci-dessus). Nous pourrions faire une classe de traversée avec le type (* -> *) -> * en utilisant quelques extensions (RankNTypes parmi eux) mais à moins que cela ne soit fait souvent, et nous obtenons une sorte de support dérivant ou TH, je ne pense pas que cela vaille la peine.

+0

'la monade ParseJSON n'est pas un transformateur ou basé sur IO'. Mais c'est un "Applicative" et "Applicative" compose. Peut-être qu'une solution basée sur 'Data.Functor.Compose IO Parser' pourrait fonctionner. – danidiaz

+0

Bien sûr, j'aimerais voir cette réponse aussi. –

+1

@danidiaz En fait, dans ma réponse, j'utilise uniquement des combinateurs 'Applicative', et j'utilise essentiellement les instances de' Compose' (mais sans le wrapper 'Compose' newtype). Mais je ne l'ai pas réalisé jusqu'à ce que vous l'ayez signalé, alors c'est un bon aperçu et un guide pour écrire le code dans un développement plus grand! –

0

Voici une autre solution, elle implique un peu plus de travail manuel, mais l'approche est assez simple - générer un IO Example aléatoire l'utiliser pour générer un "analyseur" aléatoire. Le décodage en JSON est effectué avec la fonction habituelle decode.

{-# LANGUAGE OverloadedStrings #-} 
module Test where 

import Data.Aeson 
import Data.Aeson.Types 
import System.Random 

data Example = Example {_a :: Int, _b :: Int} deriving (Show, Ord, Eq) 

getExample :: IO (Value -> Maybe Example) 
getExample = do 
ex <- randomRIO (Example 1 1, Example 10 100) 
let ex' = withObject "Example" $ \o -> 
      do a <- o .:? "a" .!= _a ex 
       b <- o .:? "b" .!= _b ex 
       return $ Example a b 
return (parseMaybe ex') 

instance Random Example where 
    randomRIO (low,hi) = Example <$> randomRIO (_a low,_a hi) 
           <*> randomRIO (_b low,_b hi) 
... 

main :: IO() 
main = do 
    getExample' <- getExample 
    let example = getExample' =<< decode "{\"a\": 20}" 
    print example 

Je ne sais pas, mais je crois que c'est la mise en œuvre plus détaillée de la solution de @ DanielWagner.

+0

re: "Je crois que c'est la mise en œuvre plus verbeuse de la solution @ DanielWagner", je pense que nos approches sont légèrement différentes (bien que les deux intéressant); vous faites un 'IO' pour générer un analyseur, alors que je fais un peu d'analyse pour générer une action' IO'. –

+0

C'est une façon intéressante de le décomposer. Nous avons trois réponses qui se résument à «analyser et générer une action d'E/S nécessaire à exécuter», «exécuter des E/S pour générer un analyseur» et «analyser pour générer un objet avec des actions E/S intégrées pour l'exécution». –