Read

The easiest way to start implementing it is to imagine that the data structure is already created and everything we need to do is to read a value from it. The obvious untyped solution will be to call trans.get("parts").get("from").get("name") . And this approach works fine until we need to update it. After the first get call, the reference to the root transaction is lost and there’ll be no way to run the update operation.

Instead, it’s possible to focus on the way of traversing the data structure without loosing the reference to the root. To accomplish this, it’s possible to implement Focus interface which holds the reference to the root and accumulates a path inside.

interface Focus<out Op> { fun narrow(k: String): Focus<Op> val op: Op }

The interesting thing that Focus is parametrised over an operation. That operation can be Read or Write depending on the context. When a leaf is reached, the typed version will finally perform an action using that operation.

narrow down the usage val f = Focus(trans) // {"root" -> Transaction, path -> []} val f2 = f.narrow("parts") // {"root" -> Transaction, path -> ["parts"]} val f3 = f2.narrow("from") // {"root" -> Transaction, path -> ["parts", "from"]} // ...

But even though the focus does its job very well, it’s completely untyped, and strings have to be used to navigate through. The type must be stored somewhere. As everyone knows that any problem can be solved with an additional layer of abstraction! Let’s define a wrapper parametrised over the type of an underlying node.

the missed layer class Cursor<out T, out Op>(val f: Focus<Op>)

Cursor is parametrised over a node type and the operation is derived from the focus. And now, the Transaction definition starts making sense. The narrowing can be delegated to the Node object that knows the type and uses the name of a property to create a new Cursor with a new Focus inside.

interface Transaction val <F> Cursor<Transaction, F>.payment by Node<Payment>()

Here, the payment is an extension property on the Transaction type which is just a marker interface. It will never be instantiated, instead by delegating property to Node<Payment> , the conversion Cursor<Transacton, F> ⇒ Cursor<Payment, F> will be made.

how Node is defined open class Node<out T> { open operator fun <Op> getValue(ref: Cursor<*, Op>, property: KProperty<*>): Cursor<T, Op> { return Cursor(ref.f.narrow(property.name)) } }

Inside Node , a new Cursor is created with the focus narrowing down using a property name. Using this technique, by just calling extension properties a focus can narrow down to the last node where the last node is delegated to Leaf instead of Node .

interface Person val <F> Cursor<Person, F>.name by Leaf<String>()

Leaf<V> is defined in the same way as Node except for the return value of getValue .

open class Leaf<out V> { open operator fun <Op> getValue(ref: Cursor<*, Op>, property: KProperty<*>): Cursor<Leaf<V>, Op> { return Cursor(ref.f.narrow(property.name)) } }

Leaf is needed to define an extension property that allows reading a value from that node. The property has the following signature val <V, T> Cursor<Leaf<V>, Read<T>>.value: V which says: given the cursor focused on a leaf and parametrised over a read operation, provide a value contained by the leaf.

The remaining logic is described below