如何在Haskell中解析具有可选变量的json字段?

问题描述 投票:-3回答:1

如何解析该文件中的输入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文件将保存到磁盘。

var jsonData = JSON.stringify(BattleMovedex  );
var fs = require('fs');
fs.writeFile("moves.json", jsonData, function(err) {
    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这样支持它。
json haskell functional-programming aeson gadt
1个回答
3
投票

使用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 (Maybe Moves) test2 = decode <$> B.readFile "moves.json"

这将这样解码您的示例:

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

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

{-# 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 (Maybe Moves) test2 = decode <$> B.readFile "moves.json"

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