如何使用来自Aeson的Parsers和IO

时间:2017-10-18 22:46:47

标签: haskell parsec aeson

我有许多字段的数据类型,如果不是由JSON配置文件手动指定,则应该随机设置。我正在使用Aeson来解析配置文件。这样做的最佳方式是什么?

目前,我设置的值等于某个不可能的值,然后检查所述值以进行编辑。

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'

我想要的是:

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

是否可以在IO Monad中定义Parsers或者使用某个随机生成器进行线程,最好是使用Aeson?

3 个答案:

答案 0 :(得分:5)

好吧,我不知道这是不是一个好主意,而且对于更大的开发来说,处理额外的IO层肯定会令人沮丧,但是下面的类型检查:

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

在最后两行中,第一行pureInt -> IO Int,第二行为IO Int -> Parser (IO Int)。在ghci:

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

答案 1 :(得分:2)

由于ParseJSON monad不是变换器或基于IO,我不知道有什么好的策略可以达到你想要的位置。您可以更轻松地做到的是解码为一种类型然后转换为第二种类型,就像之前的问题“Give a default value for fields not available in json using aeson”中所做的那样。

由于大型结构的重现繁琐,您可以对结构进行参数化,并使用IO IntInt对其进行实例化。例如,假设您希望来自网络的字段a,但{mon}中的b是随机的:

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

注意loadExample如何使用我们的traverse实例在结构内执行IO操作。以下是一个使用示例:

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

高级

如果您有多种类型的字段需要IO操作,则可以

  1. 为所有这些数据生成一种数据类型。您可以将b替换为IO Int,而不是IO MyComplexRecord。这是一个简单的解决方案。

  2. 更复杂和有趣的解决方案是使用更高类型的参数。

  3. 对于选项2,请考虑:

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

    然后,您可以使用ProxyControl.Monad.Identity代替之前使用的IO IntInt等值。您需要编写自己的遍历,因为您无法为此类派生Traverse(这就是我们上面使用的mapM)。我们可以使用类(* -> *) -> *使用一些扩展(其中包含RankNTypes)创建一个遍历类,但除非经常这样做,并且我们得到某种派生支持或TH,我认为这不值得。< / p>

答案 2 :(得分:1)

这是另一种解决方案,它涉及更多的手工劳动,但这种方法非常简单 - 生成一个随机IO Example使用它来生成一个随机的#34;解析器&#34;。使用通常的decode函数解码为JSON。

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

我不确定,但我相信这是@ DanielWagner解决方案的更详细的实现。