10

我正在考虑将一个非常简单的文本模板库移植到 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 无论如何都喜欢的东西。)

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

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

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

4

3 回答 3

15

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

Let's look at a rewrite of the JSON parser combinator from the Scala standard library or the Programming in Scala book. Instead of using Map[String, Any] to represent JSON objects, and instead of using Any to represent arbitrary JSON values, it uses an abstract data type, JsValue, to represnt JSON values. JsValue has several subtypes, representing the possible kinds of JSON values: JsString, JsNumber, JsObject, JsArray, JsBoolean (JsTrue, JsFalse), and JsNull.

处理这种形式的 JSON 数据涉及到模式匹配。由于 JsValue 是密封的,如果您没有处理所有情况,编译器会警告您。例如, 的代码toJson,一个接受 aJsValue并返回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 的提取器。很强大的东西,看看。

于 2009-04-10T07:18:27.690 回答
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]

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

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

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

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

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

type DataType[A] = Map[String, Either[Int, InnerData[A]]]

val jsData: DataType[_]

添加一些隐式转换以使 API 更漂亮,您应该会变得漂亮而花花公子。

于 2009-04-08T17:22:52.883 回答
1

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

于 2009-04-08T17:26:07.460 回答