Type Construction in Scala

    When building multi-layer ("enterprise") systems, it often turns out that they are creating ValueObject's (or case classes) in which information about any entity instance processed by the system is stored. For example, class

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


    This way of presenting data in the system has both positive properties:
    • Strongly typed data access
    • the ability to bind meta-information to properties using annotations,


    as well as some disadvantages:
    • if there are a lot of entities, then there are quite a lot of such classes, and their processing requires a lot of the same type of code (copy-paste);
    • the needs of the individual layers of the system for meta-information can be represented by annotations to the properties of this object, but the possibilities of annotations are limited and require the use of reflection;
    • if it is not necessary to provide data on all the properties of an object at once, then the created classes are difficult to use;
    • it is also difficult to imagine a change in the property value (delta).


    We want to implement a framework that allows us to create new “classes” (types, constructors of these types, objects of new types) incrementally using our own “bricks”. Along the way, taking advantage of the fact that we ourselves make “bricks”, we can achieve such useful properties:
    • the ability to describe individual properties of entities (indicating the data type in this property and any meta-information necessary for the application in a form suitable specifically for this application);
    • the ability to operate with the properties of instances in a strongly typed manner (with type checking at the compilation stage);
    • provide partial / incomplete information about the values ​​of the properties of the entity instance using the declared properties;
    • create an object type containing partial information about the properties of an entity instance. And use this type on a par with other types (classes, primitive types, etc.).


    To construct a new composite type, you need to figure out how a regular class works. You Personcan highlight components in a class declaration
    • ordered list of properties / slots (slot sequence),
    • property / slot name (slot id),
    • property / slot type.


    When using a class Personand its properties, operations can be distinguished -
    • get the value of an instance property (instance.name)
    • receiving a new instance with a changed property (since the class Personis immutable, for mutable classes, the analogy is changing the value of an object property)


    Moreover, the essence of the “first class” is the class itself Person, and its properties are the essence of the “second class”. They are not objects and we do not have the ability to operate with them abstractly.

    But we want to make properties independent entities of the "first class" from which a new "class" will be constructed.

    So, declare a property 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", ...)
    


    Such an announcement highlights the property itself, regardless of the entity in which the property will be used. Meta-information can obviously be attached to the property identifier (using an external mapping), or it can be specified directly in the object representing the property. In the latter case, data handling is somewhat simplified, although expansion with new types of meta-information is difficult.

    Slot sequence


    To get a new type, you need to collect several properties in an ordered list. To construct a type composed of others, we will use the same approach as in the HList type (from the wonderful shapeless library , for example).

    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, in the process of constructing a list of properties, we also construct a value type ( ValueType) compatible with the list of properties.

    Property Grouping


    Properties can be used as is, simply by creating a complete collection of all possible properties. However, it is better to organize properties in “clusters” - sets of properties related to one class / type of objects.

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


    This grouping can also be done using traits, which allows you to declare the same properties in different "clusters".

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


    In addition, the "clusters" allow you to automatically add an enveloping object to the meta-information of properties, which, in turn, can be very useful when processing data based on meta-information.

    Presentation of Instances


    Actually, data related to an entity can be presented in two main forms: Mapor RecordSet. Map- contains property-value pairs, while it RecordSetcontains an ordered list of properties and an array of values ​​arranged in the same order. RecordSetallows you to economically present data on a large number of instances, and Mapallows you to create a “thing in yourself” - an isolated object that contains all meta-information along with property values. Both of these methods can be used in parallel depending on current needs.

    A RecordSetwonderful structure can be used for the typed representation of strings .HList(from shapeless library, for example). We only need to form a compatible type of a during the assembly process of the ordered slot sequence HList.

    type ValueType = head.Type :: tail.ValueType
    


    To create a strongly typed Map'and we need instead of the usual class Entryuse the class SlotValue,

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


    which besides the property name and property value also contains a generic value type. This allows us to guarantee at the compilation stage that the property will receive a value of a compatible type. Itself Mapwill require a separate implementation. In the simplest case, you can use a list SlotValuethat is automatically converted to a regular Map as needed.

    Conclusion


    In addition to the basic data structure and type structure described above, auxiliary functions based on the basic tools are useful.
    • gradual construction of an instance Map(strongly typed MapBuilder);
    • lenses for access and modification of attached properties;
    • conversion Map- to RecordSetand from


    Such a framework can be used if it is necessary to process heterogeneous data based on meta-information about properties, for example:
    • work with the database:
    • the same processing of events related to the properties of various entities, for example, changing the properties of objects.

    Due to the convenience of presenting meta-information, it is possible to describe in detail all aspects of data processing without resorting to annotations.

    Code for the described designs .
    UPD: Continuation of the topic: Strictly typed representation of incomplete data

    Also popular now: