如何在Haskell中解析具有可选类型和变体类型的json?

问题描述 投票:-2回答:4

如何解析该文件中的输入json? https://github.com/smogon/pokemon-showdown/blob/master/data/moves.js

对于次要属性和标志属性?它们是可选的,并且包含变量类型。

一个最小的例子就是这个:

[
  {},
  {
    "secondary": false
  },
  {

    "secondary": {
      "chance": 10,
      "boosts": {
        "spd": -1
      }
    }
  },
  {
    "secondary": {
      "chance": 30,
      "volatileStatus": "flinch"
    }
  },
  {
    "secondary": {
      "chance": 30
    }
  },
  {
    "secondary": {
      "chance": 10,
      "self": {
        "boosts": {
          "atk": 1,
          "def": 1,
          "spa": 1,
          "spd": 1,
          "spe": 1
        }
      }
    }
  },
  {
    "secondary": {
      "chance": 10,
      "status": "brn"
    }
  },
  {
    "secondary": {
      "chance": 50,
      "self": {
        "boosts": {
          "def": 2
        }
      }
    }
  },
  {
    "secondary": {
      "chance": 100,
      "self": {}
    }
  },
  {
    "secondary": {
      "chance": 50,
      "boosts": {
        "accuracy": -1
      }
    }
  }
]

预期输出可以但不限于:1.解析的对象具有更多的结构,以便我们可以通过键访问(如果存在)2.它具有动态结构,因此我们可以在运行时检查密钥是否存在。例如,“ boosts”仅具有有限数量(<10)个可能的键。

为了方便起见,您可以选择将此代码段附加到js文件的末尾并使用node move.js运行它。两个有效的json文件将保存到磁盘。一个是json对象的列表,另一个是带有字符串作为键的对象。


var fs = require('fs');
fs.writeFile("moves_object.json", JSON.stringify(BattleMovedex), function(err) {}); // 1. save a json object with string key

var jsonList = []
for (var key of Object.keys(BattleMovedex)) {
    jsonList.push(BattleMovedex[key]);
}
fs.writeFile("moves.json", JSON.stringify(jsonList), function(err) { // 2. save as a list of json object
    if (err) {
        console.log(err);
    }
});

以锅炉板为起点:

{-# LANGUAGE OverloadedStrings, DeriveGeneric #-}

import Data.Aeson
import Data.Text
import Control.Applicative
import Control.Monad
import qualified Data.ByteString.Lazy as B
import Network.HTTP.Conduit (simpleHttp)
import GHC.Generics

-- | Type of each JSON entry in record syntax.
data Person =
  Person { firstName  :: !Text
         , lastName   :: !Text
         , age        :: Int
         , likesPizza :: Bool
           } deriving (Show,Generic)

-- Instances to convert our type to/from JSON.

instance FromJSON Person
instance ToJSON Person

-- | Location of the local copy, in case you have it,
--   of the JSON file.
jsonFile :: FilePath
jsonFile = "pizza.json"

-- | URL that points to the remote JSON file, in case
--   you have it.
jsonURL :: String
jsonURL = "http://daniel-diaz.github.io/misc/pizza.json"

-- Move the right brace (}) from one comment to another
-- to switch from local to remote.

{--
-- Read the local copy of the JSON file.
getJSON :: IO B.ByteString
getJSON = B.readFile jsonFile
--}

{--}
-- Read the remote copy of the JSON file.
getJSON :: IO B.ByteString
getJSON = simpleHttp jsonURL
--}

main :: IO ()
main = do
 -- Get JSON data and decode it
 d <- (eitherDecode <$> getJSON) :: IO (Either String [Person])
 -- If d is Left, the JSON was malformed.
 -- In that case, we report the error.
 -- Otherwise, we perform the operation of
 -- our choice. In this case, just print it.
 case d of
  Left err -> putStrLn err
  Right ps -> print ps

更具挑战性的功能部分

  1. 使用代码段生成的json是simplified版本。例如,它过滤掉原始js文件中的属性可以是functions的情况,例如onSetStatuseffect,以及genesissupernova的情况,其中self可以包含函数onHit ]。为简单起见,该问题不是必需的,但是非常有趣的是,看看像Haskell这样的函数式编程语言如何能够像Javascript这样支持它。

FYI:如果您熟悉c ++,可能会发现在这篇文章中更容易理解相同的问题:How to parse json file with type composition of std::optional and std::variant

json haskell functional-programming aeson gadt
4个回答
3
投票

NOTE:在下面的代码示例中,我使用了一个"moves.json"文件,其内容是上面的最小示例。除了可以解析任何有效JSON的getMoves之外,其他代码示例也不适用于从链接的"moves.json"文件派生的"moves.js"文件,因为格式不同(例如,它是一个对象,而不是数组) ,因为一件事)。

使用Aeson解析任意JSON的最简单方法是将其解析为Value

import Data.Aeson
import Data.Maybe
import qualified Data.ByteString.Lazy as B

getMoves :: IO Value
getMoves = do
  mv <- decode <$> B.readFile "moves.json"
  case mv of
    Nothing -> error "invalid JSON"
    Just v -> return v

任何有效的JSON都可以通过这种方式进行解析,并且生成的Value具有完全动态的结构,可以在运行时以编程方式对其进行检查。镜头库和Maybe monad在这里可能会有所帮助。例如,要查找secondary.chance为100的(第一个)对象,可以使用:

{-# LANGUAGE OverloadedStrings #-}

import Control.Lens
import Data.Aeson
import Data.Aeson.Lens
import qualified Data.Vector as Vector
import qualified Data.ByteString.Lazy as B

find100 :: Value -> Maybe Value
find100 inp = do
  arr <- inp ^? _Array
  Vector.find (\s -> s ^? key "secondary" . key "chance" . _Integer == Just 100) arr

test1 = find100 <$> getMoves

输出:

> test1
Just (Object (fromList [("secondary",Object (fromList [("chance",Number 100.0),
("self",Object (fromList []))]))]))

这是对象的Value表示形式:

{
  "secondary": {
    "chance": 100,
    "self": {}
  }
}

如果要使生成的已解析对象具有更多的结构,那么首先需要弄清楚一个Haskell表示形式,该表示形式将与您打算解析的所有可能的对象一起使用。对于您的示例,合理的表示方式[[might为:]]type Moves = [Move] data Move = Move { secondary :: Secondary' } deriving (Show, Generic) newtype Secondary' = Secondary' (Maybe Secondary) -- Nothing if json is "false" deriving (Show, Generic) data Secondary = Secondary { chance :: Maybe Int , boosts :: Maybe Boosts , volatileStatus :: Maybe String , self :: Maybe Self } deriving (Show, Generic) data Self = Self { boosts :: Maybe Boosts } deriving (Show, Generic) newtype Boosts = Boosts (HashMap.HashMap Text.Text Int) deriving (Show, Generic)

这假定所有移动都具有一个secondary字段,该字段可以是"false"或对象。它还假定可以使用许多升压键,因此在Boosts哈希图中将它们表示为任意文本字符串更为方便。另外,由于您的示例包含了两种形式的示例,因此这可以将"boosts"直接置于"secondary"之下或嵌套在"self"中,尽管这可能是一个错误。

对于这些数据类型,可以使用MoveSelfSecondary的默认实例:

instance FromJSON Move instance FromJSON Self instance FromJSON Secondary

然后使用自定义实例将Secondary'周围的Secondary新型包装器用于处理false与对象:

instance FromJSON Secondary' where parseJSON (Bool False) = pure $ Secondary' Nothing parseJSON o = Secondary' . Just <$> parseJSON o

Boosts还需要一个自定义实例,以将其解析为适当的哈希图:

instance FromJSON Boosts where parseJSON = withObject "Boosts" $ \o -> Boosts <$> mapM parseJSON o

现在,使用以下驱动程序:

test2 :: IO (Either String Moves) test2 = eitherDecode <$> B.readFile "moves.json"

这将这样解码您的示例:

> test2 Right [Move {secondary = Secondary' Nothing},Move {secondary = Secondary' (Just (Secondary {chance = Just 10, boosts = Just (Boosts (fromList [("spd",-1)])), volatileStatus = Nothing, self = ...

通过使用上面的eitherDecode,如果解析失败,我们会收到一条错误消息。例如,如果您在从"moves.json"派生的"moves.js"上运行此命令,则会得到:

> test2 Left "Error in $: parsing [] failed, expected Array, but encountered Object"

[当分析器注意到它正在尝试分析[Move]数组,但正在查找由Pokemon移动名称作为键的对象时。

这是显示两种解析类型的完整代码:

{-# OPTIONS_GHC -Wall #-} {-# LANGUAGE DeriveGeneric #-} {-# LANGUAGE DuplicateRecordFields #-} {-# LANGUAGE OverloadedStrings #-} import Control.Lens import Data.Aeson import Data.Aeson.Lens import GHC.Generics import qualified Data.Text as Text import qualified Data.HashMap.Strict as HashMap import qualified Data.Vector as Vector import qualified Data.ByteString.Lazy as B -- -- Parse into a dynamic Value representation getMoves :: IO Value getMoves = do mv <- decode <$> B.readFile "moves.json" case mv of Nothing -> error "invalid JSON" Just v -> return v find100 :: Value -> Maybe Value find100 inp = do arr <- inp ^? _Array Vector.find (\s -> s ^? key "secondary" . key "chance" . _Integer == Just 100) arr test1 :: IO (Maybe Value) test1 = find100 <$> getMoves -- -- Parse into suitable static data structures -- whole file is array of moves type Moves = [Move] data Move = Move { secondary :: Secondary' } deriving (Show, Generic) newtype Secondary' = Secondary' (Maybe Secondary) -- Nothing if json is "false" deriving (Show, Generic) data Secondary = Secondary { chance :: Maybe Int , boosts :: Maybe Boosts , volatileStatus :: Maybe String , self :: Maybe Self } deriving (Show, Generic) data Self = Self { boosts :: Maybe Boosts } deriving (Show, Generic) newtype Boosts = Boosts (HashMap.HashMap Text.Text Int) deriving (Show, Generic) instance FromJSON Move instance FromJSON Self instance FromJSON Secondary instance FromJSON Secondary' where parseJSON (Bool False) = pure $ Secondary' Nothing parseJSON o = Secondary' . Just <$> parseJSON o instance FromJSON Boosts where parseJSON = withObject "Boosts" $ \o -> Boosts <$> mapM parseJSON o test2 :: IO (Either String Moves) test2 = eitherDecode <$> B.readFile "moves.json"


0
投票
我对最小样本的尝试

0
投票
这是您的mover.json的另一种尝试

-1
投票
这是

一个不令人满意的答案

© www.soinside.com 2019 - 2024. All rights reserved.