Как мне указать тип JSON-подобных неструктурированных данных в Scala? - PullRequest
9 голосов
/ 08 апреля 2009

Я подумываю о переносе очень простой библиотеки шаблонов текстов в 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 , но, если честно, мои глаза стали немного глазеть на ковариацию / контравариантность, и я надеюсь, что кто-нибудь сможет объяснить мне это более четко и кратко.

Ответы [ 3 ]

14 голосов
/ 10 апреля 2009

Вы заметили, что вы хотите, чтобы какие-то классы дел моделировали ваши типы данных. На функциональных языках такие вещи называются «абстрактными типами данных», и вы можете прочитать все о том, как Haskell использует их, немного погуглив. В Scala-эквиваленте ADT на Haskell используются запечатанные черты и классы дел.

Давайте рассмотрим переписывание комбинатора синтаксического анализа JSON из стандартной библиотеки Scala или из книги «Программирование в Scala». Вместо использования Map [String, Any] для представления объектов JSON и вместо Any для представления произвольных значений JSON он использует абстрактный тип данных JsValue для представления значений JSON. JsValue имеет несколько подтипов, представляющих возможные виды значений JSON: JsString, JsNumber, JsObject, JsArray, JsBoolean (JsTrue, JsFalse) и JsNull.

Управление данными 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 . Очень мощный материал, зацените.

1 голос
/ 08 апреля 2009

JSON используется в качестве примера в разделе «Программирование в Scala», в главе, посвященной синтаксическому анализу комбинатора.

1 голос
/ 08 апреля 2009

Ну, есть несколько способов приблизиться к этому. Я бы, вероятно, просто использовал 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 в поле Scala с именем jsData, вы бы дали этому полю следующий тип:

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

Каждый раз, когда вы извлекаете поле из jsData, вам нужно будет сопоставить шаблон, проверяя, имеет ли значение тип Left[Int] или Right[InnerData[_]] (два подтипа Either[Int, InnerData[_]]). Как только у вас есть внутренние данные, вы должны сопоставить шаблон с с , чтобы определить, представляет ли он InnerString, InnerMap или InnerBoolean.

Технически, вы все равно должны выполнить такой тип сопоставления с образцом, чтобы использовать данные после извлечения их из JSON. Преимущество хорошо сформулированного подхода в том, что компилятор проверит вас, чтобы убедиться, что вы не упустили никаких возможностей. Недостатком является то, что вы не можете просто пропустить невозможности (например, 'eggs' сопоставление с Int). Кроме того, все эти объекты-обертки накладывают некоторые накладные расходы, так что следите за этим.

Обратите внимание, что Scala позволяет вам определять псевдоним типа, который должен сократить количество LoC, необходимое для этого:

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

val jsData: DataType[_]

Добавьте несколько неявных преобразований, чтобы сделать API красивым, и вы все должны быть хорошими и модными.

...