【问题标题】:How should I specify the type of JSON-like unstructured data in Scala?我应该如何在 Scala 中指定类 JSON 的非结构化数据的类型?
【发布时间】:2010-10-18 06:00:50
【问题描述】:

我正在考虑将一个非常简单的文本模板库移植到 scala,主要是作为学习语言的练习。该库目前在 Python 和 Javascript 中实现,其基本操作或多或少归结为(在 python 中):

template = CompiledTemplate('Text {spam} blah {eggs[1]}')
data = { 'spam': 1, 'eggs': [ 'first', 'second', { 'key': 'value' }, true ] }
output = template.render(data)

这些在 Scala 中都不是很难做到,但我不清楚的是如何最好地表达 data 参数的静态类型。

基本上,此参数应该能够包含您在 JSON 中找到的各种内容:一些原语(字符串、整数、布尔值、null),或者零个或多个项目的列表,或者零个或多个项目的映射. (出于这个问题的目的,映射可以被限制为具有字符串键,这似乎是 Scala 无论如何都喜欢的东西。)

我最初的想法只是将Map[string, Any] 用作顶级对象,但这对我来说似乎并不完全正确。事实上,我不想在其中添加任何类型的任意对象;我只想要我上面概述的元素。同时,我认为在 Java 中我真正能得到的最接近的应该是 Map<String, ?>,而且我知道 Scala 的作者之一设计了 Java 的泛型。

我特别好奇的一件事是其他具有类似类型系统的函数式语言如何处理此类问题。我有一种感觉,我在这里真正想做的是提出一组我可以进行模式匹配的案例类,但我不太能够想象它会是什么样子。

我有 在 Scala 中编程,但老实说,我的眼睛开始对协变 / 逆变的东西有点呆滞,我希望有人能更清楚、更简洁地向我解释这一点.

【问题讨论】:

    标签: json generics data-structures scala strong-typing


    【解决方案1】:

    嗯,有几种方法可以解决这个问题。我可能只会使用Map[String, Any],它应该可以很好地满足您的目的(只要地图来自collection.immutable 而不是collection.mutable)。但是,如果您真的想要经历一些痛苦,可以为此指定一个类型:

    sealed trait InnerData[+A] {
      val value: A
    }
    
    case class InnerString(value: String) extends InnerData[String]
    case class InnerMap[A, +B](value: Map[A, B]) extends InnerData[Map[A, B]]
    case class InnerBoolean(value: Boolean) extends InnerData[Boolean]
    

    现在,假设您正在将 JSON data 字段读取到名为 jsData 的 Scala 字段中,您将为该字段指定以下类型:

    val jsData: Map[String, Either[Int, InnerData[_]]
    

    每次从jsData 中拉出一个字段时,您都需要进行模式匹配,检查该值的类型是Left[Int] 还是Right[InnerData[_]]Either[Int, InnerData[_]] 的两个子类型)。获得内部数据后,您将在 that 上进行模式匹配,以确定它是否代表 InnerStringInnerMapInnerBoolean

    从技术上讲,无论如何,您都必须进行这种模式匹配,以便在您将数据从 JSON 中提取出来后使用它。良好类型方法的优点是编译器会检查您以确保您没有错过任何可能性。缺点是你不能跳过不可能的事情(比如'eggs' 映射到Int)。此外,所有这些包装对象都会产生一些开销,因此请注意这一点。

    请注意,Scala 确实允许您定义一个类型别名,这应该会减少为此所需的 LoC 数量:

    type DataType[A] = Map[String, Either[Int, InnerData[A]]]
    
    val jsData: DataType[_]
    

    添加一些隐式转换以使 API 更漂亮,你应该会很漂亮。

    【讨论】:

    • 你能详细说明一下这个 Either[Int, InnerData[A]] 类型吗?我不明白为什么它是 Int,一方面,因为我想要的原语来自集合(Int、String、Boolean、null)。谢谢!
    【解决方案2】:

    JSON 在“Scala 编程”的组合子解析一章中用作示例。

    【讨论】:

    • 我确实看到了那部分,但是生成的数据类型是一种解析树,它来自解析 JSON 字符串,我不一定要解析文字 JSON 字符串;调用render() 的代码可能包含从其他来源组装的任意数据。
    【解决方案3】:

    您发现您需要某种案例类来为您的数据类型建模。在函数式语言中,这些类型的东西被称为“抽象数据类型”,你可以通过谷歌搜索一下了解 Haskell 如何使用它们。 Scala 的 Haskell 的 ADT 等价物使用密封的特征和案例类。

    让我们看一下来自 Scala 标准库或 Programming in Scala 书中的rewrite of the JSON parser combinator。它没有使用 Map[String, Any] 来表示 JSON 对象,也没有使用 Any 来表示任意 JSON 值,而是使用抽象数据类型 JsValue 来表示 JSON 值。 JsValue 有几个子类型,代表可能的 JSON 值类型:JsStringJsNumberJsObjectJsArrayJsBooleanJsTrueJsFalse)和JsNull。 /p>

    处理这种形式的 JSON 数据涉及到模式匹配。由于 JsValue 是密封的,如果您没有处理所有情况,编译器会警告您。例如,toJson 的代码,该方法采用 JsValue 并返回该值的 String 表示,如下所示:

      def toJson(x: JsValue): String = x match {
        case JsNull => "null"
        case JsBoolean(b) => b.toString
        case JsString(s) => "\"" + s + "\""
        case JsNumber(n) => n.toString
        case JsArray(xs) => xs.map(toJson).mkString("[",", ","]")
        case JsObject(m) => m.map{case (key, value) => toJson(key) + " : " + toJson(value)}.mkString("{",", ","}")
      }
    

    模式匹配既让我们确保我们正在处理每一种情况,也让我们从其 JsType 中“解包”底层值。它提供了一种类型安全的方式来了解我们已经处理了每个案例。

    此外,如果您在编译时知道您正在处理的 JSON 数据的结构,您可以做一些非常酷的事情,例如 n8han's extractors。非常强大的东西,看看吧。

    【讨论】:

    • 谢谢,这正是我想要的。您在 pastebin 上链接到的重写 JSON 解析器的来源是什么? (我注意到 Scala 库中的内置解析器只使用 Map[String, Any]。)
    • 我在 pastebin 上编写了链接到的解析器。我一直想用它来做一个成熟的项目,但还没有找到时间。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2019-09-24
    • 2021-06-18
    • 1970-01-01
    • 1970-01-01
    • 2019-06-21
    • 2017-08-25
    • 1970-01-01
    相关资源
    最近更新 更多