Структурные типы

Некоторые варианты использования, такие как моделирование доступа к базе данных, более удобны в динамически типизированных языках, чем в статически типизированных языках. С динамически типизированными языками естественно моделировать строку как запись или объект и выбирать записи с помощью простых точечных обозначений, например row.columnName.

Достижение того же результата в статически типизированном языке требует определения класса для каждой возможной строки, возникающей в результате манипуляций с базой данных, включая строки, возникающие в результате join и проектирования, и настройки схемы для сопоставления между строкой и представляющим ее классом.

Это требует большого количества шаблонов, что заставляет разработчиков менять преимущества статической типизации на более простые схемы, в которых имена столбцов представляются в виде строк и передаются другим операторам, например row.select("columnName"). Этот подход лишен преимуществ статической типизации и все еще не так естественен, как динамически типизируемая версия.

Структурные типы (structural types) помогают в ситуациях, когда желательно поддерживать простую точечную нотацию в динамических контекстах, не теряя преимуществ статической типизации. Они также позволяют разработчикам настраивать, как должны определяться поля и методы.

Пример

Вот пример структурного типа Person:

class Record(elems: (String, Any)*) extends Selectable:
  private val fields = elems.toMap
  def selectDynamic(name: String): Any = fields(name)

type Person = Record {
  val name: String
  val age: Int
}

Тип Person добавляет уточнение (refinement) к своему родительскому типу Record, которое определяет поля name и age. Говорится, что уточнение носит структурный (structural) характер, поскольку name и age не определены в родительском типе. Но тем не менее они существуют как члены класса Person. Например, следующая программа напечатала бы "Emma is 42 years old.":

val person = Record(
  "name" -> "Emma",
  "age" -> 42
).asInstanceOf[Person]

println(s"${person.name} is ${person.age} years old.")

Родительский тип Record в этом примере представляет собой универсальный класс, который может в своем аргументе elems принимать произвольные записи. Этот аргумент - последовательность пар ключей типа String и значений типа Any. Когда создается Person как Record, необходимо с помощью приведения типов задать, что запись определяет правильные поля правильных типов. Сама Record слишком слабо типизирована, поэтому компилятор не может знать об этом без помощи пользователя. На практике связь между структурным типом и его базовым общим представлением, скорее всего, будет выполняться на уровне базы данных и, следовательно, не будет беспокоить конечного пользователя.

Record расширяет маркер trait scala.Selectable и определяет метод selectDynamic, который сопоставляет имя поля с его значением. Выбор элемента структурного типа выполняется путем вызова соответствующего метода. person.name и person.age преобразуются компилятором Scala в:

person.selectDynamic("name").asInstanceOf[String]
person.selectDynamic("age").asInstanceOf[Int]

Второй пример

Чтобы закрепить сказанное, вот еще один структурный тип с именем Book, представляющий книгу, доступную в базе данных:

type Book = Record {
  val title: String
  val author: String
  val year: Int
  val rating: Double
}

Как и в случае с Person, экземпляр Book создается следующим образом:

val book = Record(
  "title" -> "The Catcher in the Rye",
  "author" -> "J. D. Salinger",
  "year" -> 1951,
  "rating" -> 4.5
).asInstanceOf[Book]

Класс Selectable

Помимо selectDynamic класс Selectable иногда также определяет метод applyDynamic, который можно использовать для замены вызовов функций на вызов структурных элементов. Таким образом, если a является экземпляром Selectable, структурный вызов типа a.f(b, c) преобразуется в:

a.applyDynamic("f")(b, c)

Ссылки: