Как использовать парсеры от Aeson с 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))

Можно ли определить парсеры в монаде ввода-вывода или потоке через какой-то генератор случайных чисел, в идеале с использованием Aeson?


person Eric Klinkhammer    schedule 18.10.2017    source источник


Ответы (3)


Ну, я не знаю, хорошая ли это идея, и жонглирование дополнительным слоем 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)))

В каждой из последних двух строк первое pure равно Int -> IO Int, а второе — IO Int -> Parser (IO Int). В ГКИ:

> sequence (decode "{}") :: IO (Maybe Example)
Just (Example {a = 4, b = 6})
person Daniel Wagner    schedule 19.10.2017
comment
Как упоминал @danidiaz, это по существу использует экземпляры Applicative и Alternative для Compose Parser IO. Можно либо использовать этот тип напрямую, либо использовать его в качестве дальнейшего руководства для написания такого кода в более крупных разработках. - person Daniel Wagner; 19.10.2017

Я не знаю хорошей стратегии, чтобы добраться туда, где вы хотите быть, поскольку монада ParseJSON не является преобразователем или основана на IO. Что вам проще сделать, так это декодировать в один тип, а затем перевести во второй, как это было сделано в предыдущем вопросе 'Укажите значение по умолчанию для полей, недоступных в json с помощью aeson'.

Поскольку большие структуры могут быть громоздкими для воспроизведения, вы можете сделать структуру параметризованной и создать ее экземпляр с помощью IO Int или Int. Например, допустим, вам нужно поле a из проводника, а b как случайное из монады 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

Обратите внимание, как loadExample использует наш экземпляр traverse для выполнения операций ввода-вывода внутри структуры. Вот пример использования:

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

Дополнительно

Если у вас есть более одного типа поля, для которого вы хотите действие ввода-вывода, вы можете либо

  1. Сделайте один тип данных для всех. Вместо b типа IO Int вы можете сделать его IO MyComplexRecord. Это простое решение.

  2. Более сложное и забавное решение — использовать параметр типа более высокого типа.

Для варианта 2 учтите:

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

Затем вы можете использовать Proxy и Control.Monad.Identity вместо таких значений, как IO Int и Int, использовавшихся ранее. Вам нужно будет написать свой собственный обход, так как вы не можете получить Traverse для этого класса (именно это дает нам mapM, использованное выше). Мы могли бы создать класс обхода с типом (* -> *) -> *, используя несколько расширений (среди них RankNTypes), но если это не делается часто, и мы не получаем какую-то поддержку вывода или TH, я не думаю, что это стоит того.

person Thomas M. DuBuisson    schedule 19.10.2017
comment
the ParseJSON monad is not a transformer or based on IO. Но это Applicative, а сочиняют Applicative. Возможно, сработает решение, основанное на Data.Functor.Compose IO Parser. - person danidiaz; 19.10.2017
comment
Конечно, я бы тоже хотел увидеть этот ответ. - person Thomas M. DuBuisson; 19.10.2017
comment
@danidiaz На самом деле в своем ответе я использую только комбинаторы Applicative и, по сути, использую экземпляры Compose (но без оболочки Compose newtype). Но я не понимал этого, пока вы не указали на это, так что это хорошее понимание и руководство по написанию кода в более крупной разработке! - person Daniel Wagner; 19.10.2017

Вот еще одно решение, оно требует немного больше ручного труда, но подход довольно прост - сгенерировать случайный IO Example использовать его для генерации случайного "парсера". Декодирование в 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.

person epsilonhalbe    schedule 19.10.2017
comment
re: я считаю, что это более подробная реализация решения @DanielWagner, я думаю, что наши подходы немного отличаются (хотя оба интересны); вы выполняете часть IO для создания синтаксического анализатора, в то время как я выполняю некоторый анализ для создания действия IO. - person Daniel Wagner; 19.10.2017
comment
Это интересный способ разбить его. У нас есть три ответа, которые сводятся к анализу и созданию действия ввода-вывода, которое необходимо выполнить, выполнению ввода-вывода для создания синтаксического анализатора и анализу для создания объекта со встроенными действиями ввода-вывода для выполнения. - person Thomas M. DuBuisson; 19.10.2017