如何在 IO 中使用 Aeson 的解析器

How to use Parsers from Aeson with IO

我有很多字段的数据类型,如果不是由 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 中定义解析器或沿着一些随机生成器线程,最好使用 Aeson?

好吧,我不知道这是否是个好主意,并且兼顾额外的 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})

由于 ParseJSON monad 不是转换器或基于 IO,因此我不知道有什么好的策略可以到达您想去的地方。您可以更轻松地做的是解码为一种类型,然后转换为第二种类型,就像在先前的问题“”中所做的那样。

由于大型结构重现起来很麻烦,您可以将结构参数化并使用 IO IntInt 对其进行实例化。例如,假设您想要来自网络的字段 ab 来自 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

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

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

高级

如果您有不止一种类型的字段需要 IO 操作,您可以

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

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

对于选项 2,请考虑:

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

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

这是另一个解决方案,它需要更多的体力劳动,但方法非常简单 - 生成一个随机数 IO Example 使用它来生成一个随机数 "parser"。解码成 JSON 是用通常的 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

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