2017-10-18 3 views
2

Ich habe Datentypen mit vielen Feldern, die, wenn sie nicht manuell von einer JSON-Konfigurationsdatei angegeben werden, zufällig festgelegt werden sollten. Ich benutze Aeson, um die Konfigurationsdatei zu analysieren. Was ist der beste Weg, dies zu tun?Verwendung von Parsern von Aeson mit IO

Momentan setze ich Werte gleich einem unmöglichen Wert und überprüfe dann später, ob der Wert bearbeitet werden soll.

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' 

Was Ich mag würde, ist etwas entlang der Linien von:

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

Ist es möglich, Parsers im IO-Monade oder Faden entlang einiger Zufallsgenerator zu definieren, idealerweise Aeson mit?

Antwort

4

Nun, ich weiß nicht, ob es eine gute Idee ist, und Jonglieren die zusätzliche IO Schicht wird sicherlich frustrierend, wie Heck bekommt für größere Entwicklungen, aber die folgenden Typprüfungen:

{-# 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))) 

In jedem die letzten beiden Zeilen, die erste pure ist Int -> IO Int, und die zweite ist IO Int -> Parser (IO Int). In GHCI:

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

Wie von @ danidiaz erwähnt, verwendet dies im Wesentlichen die 'Applicative' und' Alternative' Instanzen für 'Compose Parser IO'. Man könnte diesen Typ entweder direkt verwenden oder ihn als weitere Anleitung zum Schreiben eines solchen Codes in größeren Entwicklungen verwenden. –

2

weiß ich nicht, eine gute Strategie zu bekommen, wo Sie seit der ParseJSON Monade nicht ein Transformator sein wollen oder auf Basis von IO. Was Sie leichter tun können, ist, in einen Typ zu decodieren und dann in den zweiten zu übersetzen, wie in einer früheren Frage 'Give a default value for fields not available in json using aeson' getan.

Da große Strukturen mühsam reproduziert werden können, können Sie die Struktur parametrisieren und mit IO Int oder Int instanziieren. Zum Beispiel, sagen wir, Sie Feld a vom Draht aber b wie zufällig aus dem IO Monade wollte:

{-# 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 

Beachten Sie, wie loadExample nutzt unsere traverse Instanz die IO-Aktionen innerhalb der Struktur auszuführen. Hier ist ein Beispiel für die Verwendung:

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

Erweiterte

Wenn Sie mehr als eine Art von Feld hatte, für die Sie ein IO-Aktion wollte man konnte entweder

  1. einen Datentyp Stellen für alle. Anstatt b vom Typ IO Int können Sie es IO MyComplexRecord machen. Dies ist die einfache Lösung.

  2. Die komplexere und unterhaltsamere Lösung besteht darin, einen höheren Typparameter zu verwenden.

Für Option 2, betrachten:

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

Sie dann Proxy und Control.Monad.Identity anstelle von Werten wie IO Int und Int bisher verwendeten nutzen könnten.Sie müssen Ihr eigenes Traversal schreiben, da Sie Traverse für diese Klasse nicht ableiten können (was uns das mapM liefert, das oben verwendet wird). Wir könnten eine Traversalklasse mit Art (* -> *) -> * mit ein paar Erweiterungen (RankNTypes unter ihnen) machen, aber wenn dies nicht oft gemacht wird und wir irgendeine Art von Unterstützung oder TH bekommen, denke ich nicht, dass es sich lohnt.

+0

'die ParseJSON Monade ist kein Transformator oder basiert auf IO'. Aber es ist ein 'Applicative' though, und' Applicative's komponieren. Vielleicht könnte eine Lösung funktionieren, die auf dem 'Data.Functor.Compose IO Parser 'basiert. – danidiaz

+0

Sicher würde ich gerne diese Antwort auch sehen. –

+1

@danidiaz In meiner Antwort verwende ich nur 'Applicative'-Kombinatoren und verwende im Wesentlichen die' Compose'-Instanzen (aber ohne den 'Compose'-Newtype-Wrapper). Aber ich habe es erst bemerkt, als du darauf hingewiesen hast, das ist ein guter Einblick und Anleitung, um den Code in einer größeren Entwicklung zu schreiben! –

0

Hier ist eine andere Lösung, es beinhaltet ein bisschen mehr Handarbeit, aber der Ansatz ist ganz einfach - erzeugen Sie eine zufällige IO Example verwenden Sie es, um einen zufälligen "Parser" zu generieren. Die Dekodierung in JSON erfolgt mit der üblichen decode Funktion.

{-# 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 

Ich bin nicht sicher, aber ich glaube, das ist die ausführlichere Implementierung von @ DanielWagner-Lösung.

+0

re: "Ich glaube, das ist die ausführlichere Implementierung von @ DanielWagners Lösung", denke ich, unsere Ansätze sind etwas anders (obwohl beide interessant); Sie machen einige 'IO', um einen Parser zu erzeugen, während ich etwas parsiere, um eine 'IO'-Aktion zu erzeugen. –

+0

Das ist ein interessanter Weg, um es zu brechen. Wir haben drei Antworten, die auf "Parsen und Erzeugen einer IO-Aktion erforderlich ausgeführt", "Ausführen von IO zum Generieren eines Parsers" und "Parse zum Generieren eines Objekts mit eingebetteten IO-Aktionen zur Ausführung" herunter laufen. –