Haskell-使用动态JSON字段建模类型的方法?

时间:2021-07-14 16:57:20

I am new to Haskell, coming from an imperative programming background. I would like to be able to serialize an object to JSON in the "Haskell way", but not quite sure how to do that yet.

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

I have read Chapter 5 of RealWorldHaskell which talks about JSON a bit, and played around with Aeson. I have also looked at a few JSON API libraries written in Haskell, such as:

我已经阅读了RealWorldHaskell的第5章,其中讨论了JSON,并与Aeson一起玩。我还看了一些用Haskell编写的JSON API库,例如:

That got me to the point of being able to create very basic JSON strings from objects (also thanks to this blog post):

这让我能够从对象创建非常基本的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

That will print out:

那将打印出来:

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

Now the goal is, add another field to a User instance that can have arbitrary fields. The Facebook Graph API has a field called data, which is a JSON object with any property you want. For example, you may make requests like this to Facebook's API (pseudocode, not familiar with the Facebook API exactly):

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

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

The first two fields, name, and object are type String, while the data field is a map of arbitrary properties.

前两个字段name和object是String类型,而data字段是任意属性的map。

The question is, what is the "Haskell way" to accomplish that on the User model above?

问题是,在上面的用户模型中实现这一目标的“Haskell方式”是什么?

I can understand how to do the simple case:

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

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

data CustomData = CustomData {
  favoriteColor :: String
}

But that isn't quite what I'm looking for. That means the User type, when serialized to JSON, will always look like this:

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

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

The question is, how do you make it so you only have to define that User type once, and then can have arbitrary fields attached to that data property, while still benefitting from static typing (or whatever is close to that, not super familiar with the details of types yet).

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

2 个解决方案

#1


4  

It depends on what you mean by arbitrary data. I'm going to extract what I think is a reasonable and non-trivial definition of "data contains an arbitrary document type" and show you a couple of possibilities.

这取决于您对任意数据的含义。我将提取我认为“数据包含任意文档类型”的合理且非平凡的定义,并向您展示几种可能性。

First I'll point to a past blog post of mine. This demonstrates how to parse documents that vary in structure or nature. Existing example here: http://bitemyapp.com/posts/2014-04-17-parsing-nondeterministic-data-with-aeson-and-sum-types.html

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

As applied to your data type, this could look something like:

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

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)

Next I'll show you to define parameterizable structure with the use of a higher kinded type. Existing example here: http://bitemyapp.com/posts/2014-04-11-aeson-and-user-created-types.html

接下来,我将向您展示如何使用更高的kinded类型定义可参数化的结构。此处的现有示例: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)

With the above code we've parameterized data and made User a higher kinded type. Now User has structured parameterized by the types of its type arguments. The data field can now be a document such as with User CustomData, a string User Text or a number User Int. You probably want a semantically meaningful type, not Int/String. Use newtype as necessary to accomplish this.

通过上面的代码,我们对参数化数据进行了参数化,并使用户获得了更高的kinded类型。现在,User已根据其类型参数的类型进行了参数化。数据字段现在可以是文档,例如User CustomData,字符串User Text或Number Int。您可能需要一个语义上有意义的类型,而不是Int / String。必要时使用newtype来完成此任务。

For a rather worked up example of how to lend structure and meaning to a data type that many would otherwise encode as (Double, Double), see https://github.com/NICTA/coordinate.

有关如何为数据类型提供结构和含义的一个相当实用的示例,许多人将编码为(Double,Double),请参阅https://github.com/NICTA/coordinate。

You can combine these approaches if you think it appropriate. It depends partly on whether you want your type to be able to express a particular, single, possibility in the type argument to the enclosing document or not.

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

I have a ton of JSON processing code and examples of how to structure data in my library at https://github.com/bitemyapp/bloodhound

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

The guiding principle is to make invalid data unrepresentable via the types to the extent possible. Consider using "smart constructors" when types alone can't validate your data.

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

See more about smart constructors here: https://www.haskell.org/haskellwiki/Smart_constructors

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

#2


2  

If you really wanted to accept a fully arbitrary JSON substructure with Aeson's FromJSON class, I'd advise that you create a field user :: Value, which is Aeson's generic type for any JSON value. If you find possible types of this JSON value later, you may convert it using FromJSON again, but initially it will hold anything that is there.

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

#1


4  

It depends on what you mean by arbitrary data. I'm going to extract what I think is a reasonable and non-trivial definition of "data contains an arbitrary document type" and show you a couple of possibilities.

这取决于您对任意数据的含义。我将提取我认为“数据包含任意文档类型”的合理且非平凡的定义,并向您展示几种可能性。

First I'll point to a past blog post of mine. This demonstrates how to parse documents that vary in structure or nature. Existing example here: http://bitemyapp.com/posts/2014-04-17-parsing-nondeterministic-data-with-aeson-and-sum-types.html

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

As applied to your data type, this could look something like:

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

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)

Next I'll show you to define parameterizable structure with the use of a higher kinded type. Existing example here: http://bitemyapp.com/posts/2014-04-11-aeson-and-user-created-types.html

接下来,我将向您展示如何使用更高的kinded类型定义可参数化的结构。此处的现有示例: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)

With the above code we've parameterized data and made User a higher kinded type. Now User has structured parameterized by the types of its type arguments. The data field can now be a document such as with User CustomData, a string User Text or a number User Int. You probably want a semantically meaningful type, not Int/String. Use newtype as necessary to accomplish this.

通过上面的代码,我们对参数化数据进行了参数化,并使用户获得了更高的kinded类型。现在,User已根据其类型参数的类型进行了参数化。数据字段现在可以是文档,例如User CustomData,字符串User Text或Number Int。您可能需要一个语义上有意义的类型,而不是Int / String。必要时使用newtype来完成此任务。

For a rather worked up example of how to lend structure and meaning to a data type that many would otherwise encode as (Double, Double), see https://github.com/NICTA/coordinate.

有关如何为数据类型提供结构和含义的一个相当实用的示例,许多人将编码为(Double,Double),请参阅https://github.com/NICTA/coordinate。

You can combine these approaches if you think it appropriate. It depends partly on whether you want your type to be able to express a particular, single, possibility in the type argument to the enclosing document or not.

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

I have a ton of JSON processing code and examples of how to structure data in my library at https://github.com/bitemyapp/bloodhound

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

The guiding principle is to make invalid data unrepresentable via the types to the extent possible. Consider using "smart constructors" when types alone can't validate your data.

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

See more about smart constructors here: https://www.haskell.org/haskellwiki/Smart_constructors

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

#2


2  

If you really wanted to accept a fully arbitrary JSON substructure with Aeson's FromJSON class, I'd advise that you create a field user :: Value, which is Aeson's generic type for any JSON value. If you find possible types of this JSON value later, you may convert it using FromJSON again, but initially it will hold anything that is there.

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