Aeson 提供灵活的数据

Aeson 提供灵活的数据

原文:https://medium.com/hackernoon/flexible-data-with-aeson-d8a23ba2169e

在某种程度上,我们的 Haskell 程序必须与网络上运行的其他程序兼容。考虑到微服务作为一种架构的使用越来越多,这一点尤其有用。不管怎样,在不同堆栈上的应用程序之间传输数据是很常见的。你需要某种格式来传输数据并在两端读取。

做这件事有许多不同的方法。但是当前 web 编程的生态系统很大程度上依赖于 JSON 格式。JSON 代表“JavaScript 对象符号”。这是一种与 Javascript 程序兼容的数据编码方式。但它也是一个任何语言都可以解析的有用的序列化系统。在本文中,我们将探索如何在 Haskell 中使用这种数据格式。

JSON 101

JSON 将数据编码成几种不同的类型。有四种基本类型:字符串、数字、布尔值和空值。这些都以可预测的方式发挥作用。这里的每一行都是有效的 JSON 值。

“Hello”4.53truefalsenull

JSON 随后提供了两种不同的组合对象的方式。第一个是数组。数组包含多个值,并用括号分隔的列表来表示它们。与 Haskell 列表不同,您可以将多种类型的对象放在一个数组中。你甚至可以把数组放入你的数组中。

[1, 2, 3][“Hello”, false, 5.5][[1,2], [3,4]]

从 Haskell 来看,这种结构似乎有点粗略。毕竟,数组中的东西的类型并不清楚。但是你可以把多类型数组看作元组,而不是列表,这样更有意义。在 JSON 中创建对象的最后也是最重要的方法是通过对象格式。对象由大括号分隔。它们就像数组一样,可以包含任意数量的值。但是,每个值也作为键-值对分配给一个字符串名称。

{
  “Name” : “Name”,
  “Age” : 23,
  “Grades” : [“A”, “B”, “C”]
}

这些对象是无限可嵌套的,所以你可以拥有数组的对象的数组等等。

编码 Haskell 类型

这很好,但是我们如何使用这种格式来传输 Haskell 数据呢?我们从一个虚拟的例子开始。我们将创建一个 Haskell 数据类型,并展示该数据的几种可能的 JSON 解释。首先,我们的类型和一些示例值:

data Person = Person
  { name :: String
  , age :: Int
  , occupation :: Occupation 
  } deriving (Show)data Occupation = Occupation
  { title :: String
  , tenure :: Int
  , salary :: Int 
  } deriving (Show)person1 :: Person
person1 = Person
  { name = “John Doe”
  , age = 26
  , occupation = Occupation
    { title = “teacher”
    , tenure = 5
    , salary = 60000
    }
  }person2 :: Person
person2 = Person
  { name = “Jane Doe”
  , age = 25
  , occupation = Occupation
    { title = “engineer”
    , tenure = 4
    , salary = 90000
    }
  }

现在我们可以选择许多不同的方式来编码这些值。最基本的方法可能是这样的:

{
  “name” : “John Doe”,
  “age” : 26,
  “occupation” : {
    “title” : “teacher”,
    “tenure” : 5,
    “salary” : 60000
    }
}{
  “name” : “Jane Doe”,
  “age” : 25,
  “occupation” : {
    “title” : “engineer”,
    “tenure” : 4,
    “salary” : 90000
  }
}

现在,我们可能希望使用动态类型语言来帮助我们的朋友。为此,我们可以围绕对象中包含的类型提供更多的上下文。这种格式可能看起来更像这样:

{
  “type” : “person”,
  “contents” : {
    “name” : “John Doe”,
    “age” : 26,
    “occupation” = {
        “type” : “Occupation”,
        “contents” : {
        “title” : “teacher”,
        “tenure” : 5,
        “salary” : 60000
        }
      }
  }
}

无论哪种方式,我们最终都必须决定与谁进行互操作的格式。不过,很可能您将与一个已经设定了他们期望的外部 API 一起工作。你必须确保你匹配他们。

数据。艾松

现在我们知道了我们的目标值是什么,我们需要一个 Haskell 表示。这来自于数据。Aeson 图书馆(以神话人物“杰森”的父亲命名)。该库包含类型Value。它在其构造函数中封装了所有基本的 JSON 类型。(为了清楚起见,我替换了几个同义词):

data Value =
  Object (HashMap Text Value) |
  Array (Vector Value) |
  String Text |
  Number Scientific |
  Bool Bool |
  Null

所以我们看到所有六种元素都被表现出来了。所以当我们想要表示我们的项目时,我们将使用这些构造函数。像许多有用的 Haskell 思想一样,我们将在这里使用 typeclasses 来提供一些结构。我们将使用两个类型类:ToJSONFromJSON。我们将首先把 Haskell 类型转换成 JSON。我们必须在我们的类型上定义toJSON函数,这将把它变成一个Value

一般来说,我们希望将我们的数据类型放入一个对象中,这个对象将有一系列的“对”。A Pair是数据。键-值对的 Aeson 表示,它由一个Text和另一个值组成。然后我们将使用object函数将这些对组合成一个 JSON Object。我们将从Occupation类型开始,因为这稍微简单一些。

{-# LANGUAGE OverloadedString #-}import Data.Aeson (ToJSON(..), Value(..), object, (.=))...instance ToJSON Occupation where
  toJSON :: Occupation -> Value
  toJSON occupation = object
    [ “title” .= toJSON (title occupation)
    , “tenure” .= toJSON (tenure occupation)
    , “salary” .= toJSON (salary occupation)
    ]

.=操作符为我们创建了一个Pair。我们所有的字段都是简单类型,它们已经有了自己的ToJSON实例。这意味着我们可以对它们使用toJSON而不是Value构造函数。还要注意,我们使用重载字符串扩展(如这里介绍的),因为我们使用字符串文字代替Text对象。一旦我们为Occupation类型定义了实例,我们就可以在 occupation 对象上调用toJSON。这使得为Person类型定义一个实例变得容易。

instance ToJSON Person where
  toJSON person = object
    [ “name” .= toJSON (name person)
    , “age” .= toJSON (age person)
    , “occupation” .= toJSON (occupation person)
    ]

现在我们可以从我们的数据类型创建 JSON 值了!一般来说,我们还希望能够解析 JSON 值,并将它们转换成我们的数据类型。我们将使用一元符号来封装失败的可能性。如果我们要寻找的键没有出现,我们想要抛出一个错误:

import Data.Aeson (ToJSON(..), Value(..), object, (.=), (.:), FromJSON(..), withObject)...instance FromJSON Occupation where
  parseJSON = withObject “Occupation” $ \o -> do
    title_ <- o .: “title”
    tenure_ <- o .: “tenure”
    salary_ <- o .: “salary”
    return $ Occupation title_ tenure_ salary_instance FromJSON Person where
  parseJSON = withObject “Person” $ \o -> do
    name_ <- o .: “name”
    age_ <- o .: “age”
    occupation_ <- o .: “occupation”
    return $ Person name_ age_ occupation_

这里有一些注释。parseJSON函数通过 eta 缩减来定义。这就是为什么似乎没有参数。.:操作符可以抓取任何符合FromJSON本身的数据类型。正如我们可以将toJSON与简单类型StringInt一起使用一样,我们也可以开箱即用地解析它们。另外,我们描述了如何解析一个Occupation,这就是为什么我们可以在 occupation 字段上使用操作符。此外,withObject函数的第一个参数是一条错误消息,如果解析失败,我们会收到这条消息。最后一点需要注意的是,我们的FromJSONToJSON实例是彼此相反的。这绝对是您在自己的 API 定义中实施的一个好属性。

现在我们有了这些实例,我们可以看到不同对象的 JSON 字节串:

>> Data.Aeson.encode person1
"{\"age\":26,\"name\":\"John Doe\",\"occupation\":{\"salary\":60000,\"tenure\":5,\"title\":\"teacher\"}}"
>> Data.Aeson.encode person2
"{\"age\":25,\"name\":\"Jane Doe\",\"occupation\":{\"salary\":90000,\"tenure\":4,\"title\":\"engineer\"}}"

派生实例

您可能会看到我们派生的实例,并认为代码看起来像样板文件。事实上,这些并不是特别有趣的实例。请记住,外部 API 可能有一些奇怪的需求。所以知道如何手工创建这些实例是很好的。不管怎样,你可能想知道是否有可能用我们可以派生EqOrd的相同方式来派生这些实例。我们可以,但是有点复杂。实际上有两种方法可以做到这一点,它们都涉及编译器扩展。第一种方式看起来更像是衍生路线。我们将最终把deriving (ToJSON, FromJSON)放入我们的类型中。但是在我们这样做之前,我们必须让它们派生出Generic typeclass。

Generic是一个类,它允许 GHC 在通用构造函数的层次上表示你的类型。要使用它,首先需要打开DeriveGeneric的编译器扩展。这允许您为数据派生泛型类型类。然后你也需要打开DeriveAnyClass扩展。一旦你完成了这些,你就可以为你的类型派生出GenericToJSONFromJSON实例。

{-# LANGUAGE DeriveGeneric     #-}
{-# LANGUAGE DeriveAnyClass    #-}…data Person = Person
  { name :: String
  , age :: Int
  , occupation :: Occupation
  } deriving (Show, Generic, ToJSON, FromJSON)data Occupation = Occupation
  { title :: String
  , tenure :: Int
  , salary :: Int
  } deriving (Show, Generic, ToJSON, FromJSON)

有了这些定义,我们将获得与手动实例相同的编码输出:

>> Data.Aeson.encode person1
"{\"age\":26,\"name\":\"John Doe\",\"occupation\":{\"salary\":60000,\"tenure\":5,\"title\":\"teacher\"}}"
>> Data.Aeson.encode person2
"{\"age\":25,\"name\":\"Jane Doe\",\"occupation\":{\"salary\":90000,\"tenure\":4,\"title\":\"engineer\"}}"

正如你所看到的,这是一个超级酷的想法,它在 Haskell 生态系统中有着广泛的用途。有许多有用的类型类具有与ToJSONFromJSON相似的模式。您需要实例来满足库约束。但是你要写的实例是非常样板化的。你可以通过结合使用泛型类型类和DeriveAnyClass得到很多这样的实例。

第二条路线涉及编写模板 Haskell。模板 Haskell 是另一个编译器扩展,允许 GHC 为你生成代码。有许多库有特定的模板 Haskell 函数。这些可以让你避免大量的样板代码,否则会非常乏味。Data.Aeson就是这些库中的一个。

首先,您需要启用模板 Haskell 扩展。然后导入Data.Aeson.TH,你就可以在你的类型上使用简单的功能deriveJSON。这将给你一些漂亮的新的ToJSONFromJSON实例。

{-# LANGUAGE TemplateHaskell #-}import Data.Aeson.TH (deriveJSON, defaultOptions)data Person = Person
  { name :: String
  , age :: Int
  , occupation :: Occupation
  } deriving (Show) data Occupation = Occupation
  { title :: String
  , tenure :: Int
  , salary :: Int
  } deriving (Show)-- The two apostrophes before a type name is template haskell syntax
deriveJSON defaultOptions ''Occupation
deriveJSON defaultOptions ''Person

我们将再次得到类似的输出:

>> Data.Aeson.encode person1
"{\"name\":\"John Doe\",\"age\":26,\"occupation\":{\"title\":\"teacher\",\"tenure\":5,\"salary\":60000}}"
>> Data.Aeson.encode person2
"{\"name\":\"Jane Doe\",\"age\":25,\"occupation\":{\"title\":\"engineer\",\"tenure\":4,\"salary\":90000}}"

不像派生这些类型整块布料,您实际上在这里有选择。注意我们最初通过了defaultOptions。我们可以改变这一点,转而传递一些修改过的选项。例如,如果需要,我们可以在字段名前添加类型:

deriveJSON (defaultOptions { fieldLabelModifier = ("occupation_" ++)}) ''Occupation
deriveJSON (defaultOptions { fieldLabelModifier = ("person_" ++)}) ''Person

产生的输出是:

>> Data.Aeson.encode person1
"{\"person_name\":\"John Doe\",\"person_age\":26,\"person_occupation\":{\"occupation_title\":\"teacher\",\"occupation_tenure\":5,\"occupation_salary\":60000}}"

模板 Haskell 可以方便。它减少了您必须编写的样板代码的数量。但是这也会使你的代码需要更长的编译时间。当您使用模板 Haskell 时,可访问性也是有代价的。大多数 Haskell 新手都知道deriving语法。但是如果你使用deriveJSON,他们可能会挠头想知道你到底在哪里定义了 JSON 实例。

编码、解码和通过网络发送

一旦我们定义了不同的实例,你可能想知道我们实际上是如何使用它们的。答案取决于您使用的库。例如,Servant库使它变得简单。除了定义实例之外,您不需要做任何序列化工作!服务端点定义它们的返回类型以及它们的响应内容类型。一旦定义了这些,序列化就会在后台进行。如果你需要做一个 API,Servant 是一个很棒的库。如果你想了解图书馆的基本情况,你应该看看我在 BayHac 2017 上的演讲。还可以看一下那个演讲中的代码示例中的一个特定示例。

现在,其他库会要求您处理 JSON 字节串。幸运的是,一旦定义了 FromJSON 和 ToJSON 实例,这也非常容易。正如我在本文的例子中所展示的,Data.Aeson具有encode功能。这将把您的 JSON 使能类型转换成一个可以通过网络发送的ByteString。很简单。

>> Data.Aeson.encode person1
"{\"name\":\"John Doe\",\"age\":26,\"occupation\":{\"title\":\"teacher\",\"tenure\":5,\"salary\":60000}}"

一如既往,解码有点棘手。您必须考虑数据格式不正确的可能性。你可以调用简单的decode函数。这为您提供了一个Maybe值,因此如果解析不成功,您将得到Nothing。在解释器中,您还应该确保指定您想要从decode得到的结果类型,否则您将得到它试图将它解析为(),这将失败。

>> let b = Data.Aeson.encode person1
>> Data.Aeson.decode b :: Maybe Person
Just (Person {name = "John Doe", age = 26, occupation = Occupation {title = "teacher", tenure = 5, salary = 60000}})

为了更好地处理错误情况,您应该选择eitherDecode。如果失败,这将给你一个错误信息。

>> Data.Aeson.eitherDecode b :: Either String Person
Right (Person {name = "John Doe", age = 26, occupation = Occupation {title = "teacher", tenure = 5, salary = 60000}})
>> let badString = "{\"name\":\"John Doe\",\"occupation\":{\"salary\":60000,\"tenure\":5,\"title\":\"teacher\"}}"
>> Data.Aeson.decode badString :: Maybe Person
Nothing
>> Data.Aeson.eitherDecode badString :: Either String Person
Left "Error in $: key \"age\" not present"

摘要

现在我们知道了将 Haskell 数据序列化为 JSON 的所有要点。第一步是为您想要序列化的类型定义ToJSONFromJSON实例。大多数情况下,这些都很容易写出来。但是也有一些不同的机制来推导它们。一旦你这样做了,像 Servant 这样的库就可以直接使用这些实例了。但是处理手动ByteString值也很容易。你可以简单地使用encode功能和decode的各种风格。

也许你总是对自己说,“Haskell 不可能对 web 编程有用。”我希望这让你看到了一些可能性。你应该试试哈斯克尔!下载我们的入门清单,获取一些有用工具的链接!

也许你已经使用了一些 Haskell,但是你担心自己在尝试 web 编程时走在了前面。你应该下载我们的递归练习册。它将帮助您巩固您的函数式编程技能!

黑客中午是黑客如何开始他们的下午。我们是 @AMI 家庭的一员。我们现在接受投稿并乐意讨论广告&赞助机会。

要了解更多信息,请阅读我们的“关于”页面在脸书上给我们点赞/发消息,或者简单地说, tweet/DM @HackerNoon。

如果你喜欢这个故事,我们推荐你阅读我们的最新科技故事趋势科技故事。直到下一次,不要把世界的现实想当然!


本站为非盈利网站,作品由网友提供上传,如无意中有侵犯您的版权,请联系删除