Type Construction in Scala
When building multi-layer ("enterprise") systems, it often turns out that they are creating
This way of presenting data in the system has both positive properties:
as well as some disadvantages:
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:
To construct a new composite type, you need to figure out how a regular class works. You
When using a class
Moreover, the essence of the “first class” is the class itself
But we want to make properties independent entities of the "first class" from which a new "class" will be constructed.
So, declare a property
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.
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).
As you can see, in the process of constructing a list of properties, we also construct a value type (
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.
This grouping can also be done using traits, which allows you to declare the same properties in different "clusters".
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.
Actually, data related to an entity can be presented in two main forms:
A
To create a strongly typed
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
In addition to the basic data structure and type structure described above, auxiliary functions based on the basic tools are useful.
Such a framework can be used if it is necessary to process heterogeneous 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 designs .
UPD: Continuation of the topic: Strictly typed representation of incomplete data
ValueObject
's (or case classes) in which information about any entity instance processed by the system is stored. For example, classcase 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
Person
can 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
Person
and 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
Person
is 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:
Map
or RecordSet
. Map
- contains property-value pairs, while it RecordSet
contains an ordered list of properties and an array of values arranged in the same order. RecordSet
allows you to economically present data on a large number of instances, and Map
allows 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
RecordSet
wonderful 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 Entry
use 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
Map
will require a separate implementation. In the simplest case, you can use a list SlotValue
that 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 typedMapBuilder
); - lenses for access and modification of attached properties;
- conversion
Map
- toRecordSet
and 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