3

次のデータ型があります。

data DocumentOrDirectory = Document DocumentName DocumentContent 
                         | Directory DirectoryName [DocumentOrDirectory]

toJSONの次のコードが付属しています。動作しますが、改善が必要です。ドキュメントとディレクトリを別々に変換する必要がありますが、その方法がわかりません。

instance JSON.ToJSON DocumentOrDirectory where
    toJSON (Document documentName documentContent) = JSON.object
        [ "document" JSON..= JSON.object 
            [ "name" JSON..= (T.pack $ id documentName)
            , "content" JSON..= (T.pack $ id documentContent)
            ]
        ]
    toJSON (Directory dirName dirContent) = JSON.object
        [ "directory" JSON..= JSON.object 
            [ "name" JSON..= (T.pack $ id dirName)
            , "content" JSON..= JSON.toJSON dirContent
            ]
        ]

JSON から DocumentOrDirectory オブジェクトを解析できる必要があります。これは私が思いついたものです(動作しません):

instance JSON.FromJSON DocumentOrDirectory where
    parseJSON (Object v@(Document documentName documentContent)) = 
        DocumentOrDirectory <$> documentName .: "name"
                            <*> documentContent .: "content"
    parseJSON (Object v@(Directory dirName dirContent) = 
        DocumentOrDirectory <$> dirName .: "name"
                            <*> dirContent .: "content"
    parseJSON _ = mzero

データを JSON との間で変換できるようにするには、既存のコードをどのように変更すればよいですか?

4

1 に答える 1

4

この問題に段階的に取り組みましょう。

最初に、例のために、名前と内容は次のように仮定しますString

type DirectoryName = String
type DocumentName = String
type DocumentContent = String

あなたはシリアライズDocumentしてDirectory別々にしたいと言っています。それ以外の場合は、それらを個別に操作したい場合もあります。それらを別々のタイプにしましょう:

data Document = Document DocumentName DocumentContent deriving Show
data Directory = Directory DirectoryName [DocumentOrDirectory] deriving Show
newtype DocumentOrDirectory = DocumentOrDirectory (Either Document Directory) deriving Show

現在、DocumentOrDirectoryはタイプ エイリアスまたはEither Document Directoryです。newtype独自のインスタンスを作成したいので、を使用しました。デフォルトEitherのインスタンスは機能しません。

そして、いくつかのヘルパー関数を定義しましょう:

liftDocument :: Document -> DocumentOrDirectory
liftDocument = DocumentOrDirectory . Left

liftDirectory :: Directory -> DocumentOrDirectory
liftDirectory = DocumentOrDirectory . Right

この定義により、個別のToJSONインスタンスを書くことができます:

instance ToJSON Document where
  toJSON (Document name content) = object [ "document" .= object [
    "name"    .= name,
    "content" .= content ]]

instance ToJSON Directory where
  toJSON (Directory name content) = object [ "directory" .= object [
    "name"    .= name,
    "content" .= content ]]

instance ToJSON DocumentOrDirectory where
  toJSON (DocumentOrDirectory (Left d))  = toJSON d
  toJSON (DocumentOrDirectory (Right d)) = toJSON d

どのようにシリアライズされているかDocumentを確認する必要がありDirectoryます (JSON 出力を整形しました):

*Main> let document = Document "docname" "lorem"
*Main> B.putStr (encode document)

{
  "document": {
    "content": "lorem",
    "name": "docname"
  }
}

*Main> let directory = Directory "dirname" [Left document, Left document]
*Main> B.putStr (encode directory) >> putChar '\n'

{
  "directory": {
    "content": [
      {
        "document": {
          "content": "lorem",
          "name": "docname"
        }
      },
      {
        "document": {
          "content": "lorem",
          "name": "docname"
        }
      }
    ],
    "name": "directory"
  }
}

同じ結果になりますB.putStr (encode $ liftDirectory directory)

次のステップは、デコーダー、FromJSONインスタンスを作成することです。キー (directoryまたはdocument) は、基になるデータがDirectoryまたはであるかどうかを示していることがわかりDocumentます。したがって、JSON 形式は重複しない (明確な) ため、解析DocumentしてからDirectory.

instance FromJSON Document where
  parseJSON (Object v) = maybe mzero parser $ HashMap.lookup "document" v
    where parser (Object v') = Document <$> v' .: "name"
                                        <*> v' .: "content"
          parser _           = mzero
  parseJSON _          = mzero

instance FromJSON Directory where
  parseJSON (Object v) = maybe mzero parser $ HashMap.lookup "directory" v
    where parser (Object v') = Directory <$> v' .: "name"
                                         <*> v' .: "content"
          parser _           = mzero
  parseJSON _          = mzero

instance FromJSON DocumentOrDirectory where
  parseJSON json = (liftDocument <$> parseJSON json) <|> (liftDirectory <$> parseJSON json)

そしてチェック:

*Main> decode $ encode directory :: Maybe DocumentOrDirectory
Just (DocumentOrDirectory (Right (Directory "directory" [DocumentOrDirectory (Left (Document "docname" "lorem")),DocumentOrDirectory (Left (Document "docname" "lorem"))])))

オブジェクト データ内の型タグを使用してデータをシリアル化できます。そうすれば、シリアル化と逆シリアル化が少し見栄えがよくなります。

instance ToJSON Document where
  toJSON (Document name content) = object [
    "type"    .= ("document" :: Text),
    "name"    .= name,
    "content" .= content ]

生成されるドキュメントは次のようになります。

{
  "type": "document",
  "name": "docname",
  "content": "lorem"
}

そしてデコード:

instance FromJSON Document where
  -- We could have guard here
  parseJSON (Object v) = Document <$> v .: "name"
                                  <*> v .= "content" 

instance FromJSON DocumentOrDirectory where
  -- Here we check the type, and dynamically select appropriate subparser
  parseJSON (Object v) = do typ <- v .= "type"
                            case typ of
                              "document"  -> liftDocument $ parseJSON v
                              "directory" -> liftDirectory $ parseJSON v
                              _           -> mzero

サブタイプを持つ言語では、そのようなscalaは次のようになります:

sealed trait DocumentOrDirectory
case class Document(name: String, content: String) extends DocumentOrDirectory
case class Directory(name: String, content: Seq[DocumentOrDirectory]) extends DocumentOrDirectory

このアプローチ (サブタイピングに依存する) の方が便利であると主張する人もいるかもしれません。Haskell では、より明示的です。オブジェクトについて考えるのが好きな場合は、明示的な型強制 / アップキャストと考えることがliftDocumentできます。liftDirectory


編集: 要点としての作業コード

于 2014-12-25T15:04:56.313 に答える