Конструирование типов в Scala

При построении многослойных («enterprise») систем часто оказывается, что создаются ValueObject'ы (или case class'ы), в которых хранится информация о каком-либо экземпляре сущности, обрабатываемом системой. Например, класс

case class Person(name: String, address: Address)


Такой способ представления данных в системе обладает как положительными свойствами:


так и некоторыми недостатками:


Мы хотим реализовать фреймворк, позволяющий создавать новые «классы» (типы, конструкторы этих типов, объекты новых типов) инкрементно, используя наши собственные «кирпичики». Попутно, пользуясь тем, что мы сами изготавливаем «кирпичики», мы можем достичь таких полезных свойств:


Чтобы сконструировать новый составной тип, надо разобраться, как устроен обычный класс. В объявлении класса Person можно выделить компоненты


При использовании класса Person и его свойств можно выделить операции —


При этом сущностью «первого класса» является сам класс Person, а его свойства — сущности «второго класса». Они не являются объектами и мы не имеем возможности оперировать с ними отвлечённо.

Мы же хотим свойства сделать самостоятельными сущностями «первого класса», из которых уже будет конструироваться новый «класс».

Итак, объявим свойство name:

trait SlotId[T]

case class SlotIdImpl[T](slotId:String, ...) extends SlotId[T]

def slot[T](slotId:String, ...) = SlotIdImpl[T](slotId, ...)

val name = slot[String]("name", ...)


Такое объявление выводит на первый план само свойство безотносительно той сущности, в которой свойство будет использоваться. Метаинформация может очевидным образом привязываться к идентификатору свойства (с использованием внешнего отображения), либо указываться непосредственно в объекте, представляющем свойство. В последнем варианте несколько упрощается оперирование данными, хотя расширение новыми видами метаинформации затруднено.

Последовательность слотов


Чтобы получить новый тип, надо собрать несколько свойств в упорядоченный список. Для конструирования типа, составленного из других, будем использовать такой же подход, как в типе HList (из замечательной библиотеки shapeless, например).

sealed trait SlotSeq {
   type ValueType <: HList
}
case object SNil extends SlotSeq {
   type ValueType = HNil
}
case class ::[H<:SlotId, T<:SlotSeq](head:H, tail : T) extends SlotSeq {
   type ValueType = H :: T#ValueType
}


Как видно, в процессе конструирования списка свойств мы также конструируем тип значения (ValueType), совместимого со списком свойств.

Группировка свойств


Свойства можно использовать как есть, просто создавая полную коллекцию всех возможных свойств. Однако лучше организовать свойства в «грозди» — наборы свойств, относящихся к одному классу/типу объектов.

object PersonType {
  val name = slot[String]("name", ...)
  val address ...
  ...
}


Такую группировку также можно делать с помощью trait'ов, что позволяет объявлять одинаковые свойства в разных «гроздях».

trait Identifiable {
  val id = slot[Long]("id")
}

object Employee extends Identifiable


Кроме того, «грозди» позволяют в метаинформацию свойств автоматически добавлять охватывающий объект, что, в свою очередь, может быть весьма полезно при обработке данных на основе метаинформации.

Представление экземпляров


Собственно, данные, относящиеся к сущности, могут быть представлены в двух основных формах: Map или RecordSet. Map — содержит пары свойство-значение, в то время как RecordSet содержит упорядоченный список свойств и массив значений, расположенных в том же порядке. RecordSet позволяет экономно представить данные о большом количестве экземпляров, а Map позволяет создать «вещь в себе» — изолированный объект, который содержит всю метаинформацию вместе со значениями свойств. Оба этих способа могут использоваться параллельно в зависимости от текущих потребностей.

Для типизированного представления строк RecordSet'а может использоваться замечательная структура HList (из библиотеки shapeless, например). Нам надо лишь в процессе сборки упорядоченного slot sequence'а формировать тип совместимого HList'а.

type ValueType = head.Type :: tail.ValueType


Для создания строготипизированного Map'а нам потребуется вместо обычного класса Entry использовать свой класс SlotValue,

case class SlotValue[T](slot:SlotId[T], value:T)


который кроме имени свойства и значения свойства также содержит generic тип значения. Это позволяет уже на этапе компиляции гарантировать, что свойство получит значение совместимого типа. Сам Map потребует отдельной реализации. В простейшем случае, можно использовать список SlotValue, который автоматически конвертируется в обычный Map по мере необходимости.

Заключение


Кроме вышеописанной базовой структуры данных и структуры типов, полезными являются вспомогательные функции, построенные на базовом инструментарии


Такой фреймворк может применяться при необходимости обработки разнотипных данных на основе метаинформации о свойствах, например:

За счёт удобства представления метаинформации, можно детально описать все аспекты обработки данных, не прибегая к аннотациям.

Код для описанных конструкций .
UPD: Продолжение темы: Строго типизированное представление неполных данных
@primetalk
13.05.2014 11:33 UTC
Первоисточник