📜 ⬆️ ⬇️

Type construction in Scala

When building multi-layer (“enterprise”) systems, it often turns out that ValueObject (or case classes) are created that store information about any instance of the entity being processed by the system. For example, the class

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


This way of presenting data in the system has the following positive properties:

')
and some disadvantages:


We want to implement a framework that allows you to create new "classes" (types, constructors of these types, objects of new types) incrementally, using our own "building blocks". Along the way, using the fact that we ourselves manufacture "bricks", we can achieve these useful properties:


To construct a new composite type, it is necessary to figure out how a regular class works. In the declaration of the class Person you can select components


When using the Person class and its properties, you can select operations -


In this case, the essence of the "first class" is the class of Person , and its properties - the essence of the "second class". They are not objects and we are not able to operate with them abstractly.

We want to make properties independent entities of the “first class”, from which a new “class” will be constructed.

So, let's declare the name property:

 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 brings to the fore the property itself, regardless of the entity in which the property will be used. Meta-information can obviously be tied to a property identifier (using an external display), or specified directly in the object that represents the property. In the latter version, the handling of data is somewhat simplified, although the expansion of 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 remarkable 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 property list, we also construct a value type ( ValueType ) that is compatible with the property list.

Property grouping


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

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


This grouping can also be done with the help of 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 a covering object to the meta-information of properties, which, in turn, can be quite useful when processing data based on meta-information.

Representation of instances


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

For the typed representation of RecordSet 'strings, the remarkable HList structure can be used (from the shapeless library, for example). We only need to build a compatible HList type in the process of assembling an ordered slot sequence.

 type ValueType = head.Type :: tail.ValueType 


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

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


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

Conclusion


In addition to the above-described basic data structure and type structure, auxiliary functions based on basic tools are useful.


Such a framework can be used when it is necessary to process various types of data based on meta-information about properties, for example:

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 constructions .
UPD: Continuing the theme: Strongly typed representation of incomplete data

Source: https://habr.com/ru/post/222553/


All Articles