How to use Parsers from Aeson with IO

非 Y 不嫁゛ 提交于 2019-12-05 20:01:49

Well, I don't know if it's a good idea, and juggling the extra IO layer will certainly get frustrating as heck for larger developments, but the following type-checks:

{-# 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 each of the last two lines, the first pure is Int -> IO Int, and the second is IO Int -> Parser (IO Int). In ghci:

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

I don't know of a good strategy to get where you want to be since the ParseJSON monad is not a transformer or based on IO. What you can more easily do is decode into one type then translate into the second as done in a prior question 'Give a default value for fields not available in json using aeson'.

Since large structures can be cumbersome to reproduce you could make the structure parameterized and instantiate it with either IO Int or Int. For example, let's say you wanted field a from the wire but b as random from the IO monad:

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

Notice how loadExample uses our traverse instance to execute the IO actions inside the structure. Here is an example use:

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

Advanced

If you had more than one type of field for which you wanted an IO action you could either

  1. Make one data type for all of them. Instead of b being type IO Int you might make it IO MyComplexRecord. This is the easy solution.

  2. The more complex and fun solution is to use a higher kind type parameter.

For option 2, consider:

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

You could then use Proxy and Control.Monad.Identity instead of values like IO Int and Int used previously. You'll need to write your own traversal since you can't derive Traverse for this class (which is what gives us the mapM used above). We could make a traversal class with kind (* -> *) -> * using a few extensions (RankNTypes among them) but unless this is done often, and we get some sort of deriving support or TH, I don't think that is worthwhile.

Here is another solution, it involves a bit more manual labour, but the approach is quite straightforward - generate a random IO Example use it to generate a random "parser". Decoding into JSON is done with the usual decode function.

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

I am not sure but I believe this is the more verbose implementation of @DanielWagner's solution.

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!