4

我是 Haskell 的新手,来自命令式编程背景。我希望能够以“Haskell 方式”将对象序列化为 JSON,但还不太确定如何做到这一点。

我已经阅读了 RealWorldHaskell 的第 5 章,其中谈到了一些关于 JSON 的内容,并与 Aeson 一起玩过。我还查看了一些用 Haskell 编写的 JSON API 库,例如:

这让我能够从对象创建非常基本的 JSON 字符串(也感谢这篇博文):

{-# LANGUAGE OverloadedStrings, DeriveGeneric #-}

import Data.Aeson
import GHC.Generics

data User = User {
  email :: String,
  name :: String
} deriving (Show, Generic)

instance ToJSON User

main = do
  let user = User "foo@example.com" "Hello World"
  let json = encode user
  putStrLn $ show json

那将打印出:

"{\"email\":\"foo@example.com",\"name\":\"Hello World\"}"

现在的目标是,将另一个字段添加到User可以具有任意字段的实例中。Facebook Graph API 有一个名为 的字段data,它是一个 JSON 对象,具有您想要的任何属性。例如,您可能会向 Facebook 的 API 发出这样的请求(伪代码,不熟悉 Facebook API):

POST api.facebook.com/actions
{
  "name": "read",
  "object": "book",
  "data": {
    "favoriteChapter": 10,
    "hardcover": true
  }
}

前两个字段 ,nameobject是 type String,而data字段是任意属性的映射。

问题是,在上述User模型上实现这一目标的“Haskell 方式”是什么?

我可以理解如何做简单的案例:

data User = User {
  email :: String,
  name :: String,
  data :: CustomData
} deriving (Show, Generic)

data CustomData = CustomData {
  favoriteColor :: String
}

但这并不是我想要的。这意味着User当序列化为 JSON 时,该类型将始终如下所示:

{
  "email": "",
  "name": "",
  "data": {
    "favoriteColor": ""
  }
}

问题是,你是如何做到的,所以你只需要定义User一次该类型,然后可以将任意字段附加到该data属性,同时仍然受益于静态类型(或任何接近该类型的东西,对细节不太熟悉类型呢)。

4

2 回答 2

4

这取决于您所说的任意数据是什么意思。我将提取我认为是“数据包含任意文档类型”的合理且重要的定义,并向您展示几种可能性。

首先,我将指向我过去的一篇博客文章。这演示了如何解析结构或性质不同的文档。现有示例:http: //bitemyapp.com/posts/2014-04-17-parsing-nondeterministic-data-with-aeson-and-sum-types.html

应用于您的数据类型时,这可能类似于:

data CustomData = NotesData Text | UserAge Int deriving (Show, Generic)
newtype Email = Email Text deriving (Show, Generic)
newtype Name  = Name  Text deriving (Show, Generic)

data User = User {
  email :: Email,
  name  :: Name,
  data  :: CustomData
} deriving (Show, Generic)

接下来,我将向您展示如何使用更高种类的类型来定义参数化结构。现有示例:http: //bitemyapp.com/posts/2014-04-11-aeson-and-user-created-types.html

newtype Email = Email Text deriving (Show, Generic)
newtype Name  = Name  Text deriving (Show, Generic)

-- 'a' needs to implement ToJSON/FromJSON as appropriate
data User a = User {
  email :: Email,
  name  :: Name,
  data  :: a
} deriving (Show, Generic)

使用上面的代码,我们已经参数化data并制作User了更高种类的类型。现在User已经通过其类型参数的类型进行了结构化参数化。该data字段现在可以是文档,例如 with User CustomData、字符串User Text或数字User Int。您可能想要一个语义上有意义的类型,而不是 Int/String。根据需要使用 newtype 来完成此操作。

有关如何将结构和含义赋予许多人将编码为(Double,Double)的数据类型的相当完善的示例,请参阅https://github.com/NICTA/coordinate

如果您认为合适,您可以结合使用这些方法。这部分取决于您是否希望您的类型能够在封闭文档的类型参数中表达特定的、单一的可能性。

我在https://github.com/bitemyapp/bloodhound有大量 JSON 处理代码和如何在我的库中构造数据的示例

指导原则是尽可能通过类型使无效数据无法表示。当单独的类型无法验证您的数据时,请考虑使用“智能构造函数”。

在此处查看有关智能构造函数的更多信息:https ://www.haskell.org/haskellwiki/Smart_constructors

于 2014-11-02T02:18:18.973 回答
2

如果您真的想使用 Aeson 的 FromJSON 类接受完全任意的 JSON 子结构,我建议您创建一个字段 user :: Value,这是 Aeson 用于任何 JSON 值的通用类型。如果您稍后发现此 JSON 值的可能类型,您可以再次使用 FromJSON 对其进行转换,但最初它将保存那里的任何内容。

于 2014-11-02T08:32:59.703 回答