Types Construction in Scala

Scala

When constructing “enterprise” systems it often turns out that ValueObjects (or case classes) are created. They store information about some instance of entity that is processed by the system. For example:

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

This way of data representation in the system has two major strengths:

  • Strictly typed access to the data;
  • Capability of meta information binding to features with the help of annotations.

And drawbacks: - If there are many entities, the number of such classes also increases and their processing requires a lot of code of the same type (copy-paste); - Meta information can be represented by annotations to the object features. But annotations facilities are limited and need reflection use; - If the data should be represented not about all object features at once, it’s difficult to use the created classes; - It’s also difficult to introduce a change of feature value (delta).

We want to implement a framework that will allow creating new “classes” (types, types’ constructors, objects of the new types) incrementally using our own “bricks”. Using our own “bricks” production we can reach the following strengths:

  • Capability of describing separate features of entities (indicating data type in this feature and any meta information which application requires, in the fitting it form);
  • Capability of operating with instance features in a strictly typed way (with types check-up at the compilation stage);
  • Provide partial/incomplete information about the entity instance features, using the declared features;
  • Create an object type which will contain partial information about the entity instance features. Use this type along with other types (classes, primitive types, etc).

In order to construct a new compound type we should clarify how a common class is arranged. In Person class declaration the following components can be distinguished:

  • slot/feature sequence,
  • slot feature id,
  • slot/feature type.

When using Person class and its features, the following operation can be distinguished: - Acquiring a value of an instance feature (instance.name), - Acquiring new instance with the changed feature (Person class is immutable. The change of an object feature value is an analogue for mutable classes.

“The first class” entity is Person class. Its features are the entities of “the second class”. They are not objects and we can’t operate abstractedly with them.

What we want is to make features self-contained entities of the “first class” and design a new class of them.

Declare name feature:

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", ...)

Such declaration exposes the feature itself irrelative of the entity this feature will be used in. Meta information can either be in an obvious way tied to the feature identifier (using the external map), or indicated in the object that represents the feature. Data processing is a bit simplified in the last variant, though enhancement by new types of meta information is complicated.

Slot Sequence

In order to get a new type we should build several features into an ordered list. In order to construct a type, aggregated from other types, we’ll use the same method as in HList type (for example, from a great shapeless library)

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
}

As you can see, when constructing a feature list we are also constructing a value type (ValueType), which is compatible with the feature list.

Feature Grouping

Features can be used as they are just by creating a complete collection of all possible features. But it’s better to organize them into “clusters”. They are feature sets that relate to the same object class/type.

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

Such grouping can be also made with the help of traits. This allows us to declare same features in different “clusters”

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

object Employee extends Identifiable

Besides, “clusters” enable us to automatically add a wrapper object to the features of meta information. This can be quite useful when processing the data on the basis of meta information.

Instance Representation

The data referring to an entity can be represented in two main forms: Map or RecordSet. Map contains feature-value pairs, while RecordSet contains an ordered list of features and an array of values that are arranged in the same order. RecordSet allows to economically represent the data about a great number of instances, while Map allows to create “a thing in itself” – a self-contained object, which contains all meta information with features values. These two methods can be both used in parallel depending on the current requirements.

In order to represent RecordSet lines typified HList can be used (for example, from shapeless library). During the ordered slot sequence building form a compliant HList type.

type ValueType = head.Type :: tail.ValueType

In order to create a strictly typified Map, we’ll need to use our own SlotValue class instead of Entry class. case class SlotValue[T](slot:SlotId[T], value:T)

Besides feature name and value, it also contains generic value type. This allows us to guarantee at the stage of compilation that the feature will get a compatible type value. Map will require a separate implementation. In the easiest case SlotValue list can be used. It’s automatically converted to the general Map when it’s necessary.

Summary

Besides the mentioned above basic data and types structures there are also useful helper functions that are based on the basic toolset

  • A step-by-step construction of Map instance (a strictly typified MapBuilder);
  • Lens for the access and modification of the enclosed features;
  • Map convertion to RecordSet and vice versa.

Such framework can be applied when processing polymorphic data on the basis of meta information about the features, foe example:

  • work with the database:;
  • the same type of processing of the events, which refer to the features of different entities. For example, the change of objects’ features.

By using the meta information presentation facility all aspects of data processing can be described in details without using annotations.

[described constructions on github] [original source]

Comments