Blog

In this article I will try to describe a new interesting phenomenon which appeared as a by-product of fascinating progress made by Kotlin development team. Namely, the new approach to library and application architecture design, which I call context-oriented programming.

A few words about function resolution

It is well known, that there are three main programming paradigms (pedant's comment: there are other paradigms as well):

  • Procedural programming
  • Object-oriented programming
  • Functional programming

All those approaches work with functions with one or another way. Let’s look on them from the point of function resolution or dispatch (meaning which function will be used in which case). Procedural programming tends to use global functions and resolve them statically based on the name and argument type. Of course types have meaning only in case of statically typed languages. In python, for example one calls function by name and if the parameters are wrong, an exception will be thrown in runtime. The function resolution in procedural language is based on procedure/function name and its parameters only and in most cases static.

Object-oriented programming style  tends to limit function visibility. Functions are not global, but instead are members of objects which means that they could be called only on the instance of a specific object (pedant's comment: some classic procedural languages have module system and therefore visibility scopes, procedural language != C). Of course one can always replace a object member function with global function with additional argument with the type of the calling object, but from syntactic point of view the difference is rather large. For example, now methods are grouped by the object they are called upon and so one could clearly see what methods or behaviors are supported by this type of object. Of course, the most important part is the encapsulation which means that some of object fields or behaviors could be private and accessible only from this object members (you can’t do it in purely procedural approach) and polymorphism, which means that actually used method is decided not based upon the method name, but also on runtime type of the object it is called upon. Object-oriented dispatch is based on caller runtime type, method name and compile-time types of arguments.

Functional programming does not bring anything principally new in terms of function resolution. Usually, functional-oriented languages have better scoping rules (pedant's comment: again, not all procedural languages are C, so there are some with good scoping), which allow to perform more fine-grained visibility control for functions based on module system, but overwise, the dispatch is performed based on compile-time types of arguments.

What is this?

In case of object programming, when we call an object method, we have all of its parameters, but additionally we have an explicit (in case of Python) or implicit parameter representing the instance of calling class (here and later all examples are written in Kotlin):

class A{
    fun doSomething(){
        println("This method is called on $this")
    }
}

Nested classes and closures complicate things a bit:

interface B{
    fun doBSomething()
}

class A{
    fun doASomething(){
        val b = object: B{
            override fun doBSomething(){
                println("This method is called on $this inside ${this@A}")
            }
        }
        b.doBSomething()
    }
}

In this case there are two implicit this parameters for function doBSomething. One comes form the instance of class B and another one comes form enclosing A instance. The same works for much more common case of lambda closure. The important note about this is that it works not only as an implicit parameter, but also as a scope or context for all functions and objects called in a lexical scope where it is defined. Meaning that method doBSomething in fact has access to any public or private members of A as well as members of B itself.

Here comes Kotlin

Kotlin brings a completely new toy to the playground: extension functions. In Kotlin one can define an extension function like A.doASomething() which could be defined anywhere in the program, not just inside of A. Inside this function one has implicit this parameter called receiver and pointing to the instance of A on which the method is called:

class A

fun A.doASomthing(){
    println("This extension method is called on $this")
}

fun main(){
    val a = A()
    a.doASomthing()
}

Extension function do not have access to private members of its receiver, so it does not violate encapsulation.

The next important thing Kotlin brings on the table is a scoped code blocks. One can run an arbitrary code block using something as a receiver:

class A{
    fun doInternalSomething(){}
}

fun A.doASomthing(){}

fun main(){
    val a = A()
    with(a){
        doInternalSomething()
        doASomthing()
    }
}

In this example both functions could be called without additional a. at the beginning, because withfunction moves all code in the following block inside a context. Which means that all function in this block are called as if they are called on provided a object.

The final (at this moment) step to context-oriented programming is called member extension. In this case an extension function is defined inside another class, like this:

class B

class A{
    fun B.doBSomething(){}
}

fun main(){
    val a = A()
    val b = B()
    with(a){
        b.doBSomething() // this will work
    }
    b.doBSomething() // this will throw exception
}

The important thing is that B acquires some new behaviors, but only when called in specific parametric lexical context. The member extension function is equal member of class A, which means that the implementation of this function could be different depending on specific instance of A passed as a context. It could even interact with some state of a object.

Context-oriented dispatch

At the beginning of the article I’ve spent some time discussing different function dispatch approaches. The reason behind this preamble is that extension functions in Kotlin allow to define function dispatch in a new way. Now we can say that decision about which function to use is based not only on type of its parameters, but also on the lexical context where it is called. Meaning that the same expression in different contexts could have different meaning. Of course, from implementation point of view, nothing changed, we still have explicit receiver object which governs the dispatch for itself and other objects mentioned in its member extensions, but from point of view of syntax, the approach is different.

For example of how context-oriented approach is different from classic object-oriented one, let us consider classic Java problem of arithmetic operations on generic numbers. The Number class in Java and Kotlin is a parent class for all numbers, but contrary to specialized numbers like Double it does not define its own arithmetic operations. So one could not write something like:

val n: Number = 1.0

n + 1.0 // the `plus` operation is not defined on `Number`

The reason behind this is that there is no way to define arithmetic operations consistently on all type of numbers. For example Integer division is different from floating point division. In some special cases, user knows which type of operations he wants, but usually it does not make sense to define them globally. The object-oriented (and in fact functional) solution is to define a new type on top of our Number class, define operations for it and use it wherever it is needed (in Kotlin 1.3 it could be done by using inline classes). Instead let us define a context with those operations and apply it locally:

interface NumberOperations{
    operator fun Number.plus(other: Number) : Number
    operator fun Number.minus(other: Number) : Number
    operator fun Number.times(other: Number) : Number
    operator fun Number.div(other: Number) : Number
}

object DoubleOperations: NumberOperations{
    override fun Number.plus(other: Number) = this.toDouble() + other.toDouble()
    override fun Number.minus(other: Number) = this.toDouble() - other.toDouble()
    override fun Number.times(other: Number) = this.toDouble() * other.toDouble()
    override fun Number.div(other: Number) = this.toDouble() / other.toDouble()
}

fun main(){
    val n1: Number = 1.0
    val n2: Number = 2

    val res = with(DoubleOperations){
        (n1 + n2)/2
    }
    
    println(res)
}

In this case the calculation of res is done inside the context, which defines additional operations. The context is not necessary is defined locally, it could be passed implicitly as a function receiver. For example we can do it in a following way:

fun NumberOperations.calculate(n1: Number, n2: Number) = (n1 + n2)/2

val res = DoubleOperations.calculate(n1, n2)

This means that the logic inside context is completely separated from the context implementation and could be written in different part of the program or even different module. In this simple example, context itself is a singleton without state, but it is possible to use stateful contexts.

Also one need to remember, that contexts could be nested:

with(a){
    with(b){
        doSomething()
    }
}

Effectively combining the behaviors from two classes, but this feature currently is hard to control due to lack of multi-receiver extensions (KT-10468).

The power of explicit coroutines

Surprisingly, one of the best examples of context-oriented approach is already used in the language by coroutine library. The idea itself is explained in detail by Roman Elizarov in his article. Here I would only like to highlight that CoroutineScope is a typical case of context-oriented design with stateful context. CoroutineScope plays two roles:

  • It contains CoroutineContext which is necessary for running coroutines and is inherited when new coroutine is started.
  • It contains the state of parent coroutine means to cancel parent coroutine if child one is failed.

Also, structured concurrency gives the best example of context oriented architecture:

suspend fun CoroutineScope.doSomeWork(){}

GlobalScope.launch{
    launch{
        delay(100)
        doSomeWork()
    }
}

Here doSomeWork is a context-based function defined outside its context. launch methods create two nested contexts which are equivalent to lexical scopes of corresponding functions (in this case both contexts have the same type, so inner context shadows outer one). The good starting point for someone, who wants to start to work with coroutines is the official guide.

DSL

There is a broad class of problems in Kotlin which are usually called DSL problems. Usually, by DSL people mean some code which provides a user-friendly builder to create some kind of complicated internal structure. It is not quite correct to call those builder DSLs since they use basic Kotlin syntax without any tweaks, but let’s stick to common term.

DSL-builders in most cases are context oriented. For example if you want to create an HTML element you need first to check whether it is possible to add this particular element in this particular place. kotlinx.htmllibrary achieves it by providing context-based extensions of classes representing specific tags. In fact, the whole library is made of context extensions for existing DOM elements.

Another example is TornadoFX GUI builder. The whole scene graph builder is organized as a sequence of nested context-builder, where inner blocks are responsible for building children for outer blocks or adjusting parent properties. Here is an example from official documentation:

override val root = gridPane{
    tabpane {
        gridpaneConstraints {
            vhGrow = Priority.ALWAYS
        }
        tab("Report", HBox()) {
            label("Report goes here")
        }
        tab("Data", GridPane()) {
            tableview<Person> {
                items = persons
                column("ID", Person::idProperty)
                column("Name", Person::nameProperty)
                column("Birthday", Person::birthdayProperty)
                column("Age", Person::ageProperty).cellFormat {
                    if (it < 18) {
                        style = "-fx-background-color:#8b0000; -fx-text-fill:white"
                        text = it.toString()
                    } else {
                        text = it.toString()
                    }
                }
            }
        }
    }
}

Here lexical scope defines a context of its own (a quite logical one, since it represents a section of GUI and its internals) and has access to parent contexts.

What next: multiple receivers

The context-oriented programming provides a lot of tools for Kotlin developer and opens a new way to design code architecture, but do we need something more?

We probably do.

Current development in context-driven way is limited by the fact that one needs to define member extension in order to get some context-scoped behavior of class. It is OK, when it is a user-defined class, but what if we need to define some context-scoped behavior for a library class? Or if we want to create extension for already scoped behavior (for example add some extension inside CoroutineScope)? Currently Kotlin does not allow more then one receiver on an extension function. But multiple receivers could be added to language in a non-breaking backward-compatible way. The multiple receivers feature is currently being discussed (KT-10468) and will lead to a KEEP request in nearest future. The problem (or maybe the feature) of nested contexts is that it allows to cover most if not all use-cases of type-classes, another sought-after possible feature. It is quite unprobable that both features will be implemented in the language at the same time.

Addendum

I would like to thank our friendly neighborhood Haskell-loving pedant Alexey Khudyakov for his remarks on the text and correction of my rather unconstrained usage of terms. 

Задача: есть слоистая структура, в каждом слое по определенному пути может находится 0, 1 или много элементов. Надо все эти элементы сгруппировать по определенному правилу и в каждой группе слить в один и вернуть список того, что получилось.

В чисто процедурном подходе это можно сделать, но это будет кошмар на сотню строк, при этом, разумеется, никакой истинной универсальности добиться не получится, поскольку нет возможности передать правила по объединению элементов. Максимум, можно запрограммировать несколько фиксированных преобразований.

В чисто объектном подходе можно сделать универсальную реализацию. На каждый вызов надо будет конструировать новый класс, наследующий интерфейс преобразования. В принципе, ничего фатального, но  много лишнего класса.

Вот как это выглядит с применением функциональных элементов:

Пример
/**
 * Merge node lists grouping nodes by provided classifier and then merging each group independently
 *
 * @param nodeName   the name of node
 * @param classifier grouping function
 * @param collector  used to each group
 * @param <A>        intermediate collector accumulator type
 * @param <K>        classifier key type
 * @return
 */
public <A, K> Collection<Meta> collectNodes(String nodeName, Function<? super Meta, ? extends K> classifier, Collector<Meta, A, Meta> collector) {
    return layers().stream()
            .filter(layer -> layer.hasMeta(nodeName))
            .flatMap(layer -> layer.getMetaList(nodeName).stream())
            .collect(Collectors.groupingBy(classifier, () -> new LinkedHashMap<>(), collector)).values();
    //linkedhashmap ensures ordering
}

Ну и несколько утилок для удобства:

Утилки
/**
 * Same as above, but uses fixed replace rule to merge meta
 *
 * @param nodeName
 * @param classifier
 * @param <K>
 * @return
 */
public <K> Collection<Meta> collectNodes(String nodeName, Function<? super Meta, ? extends K> classifier) {
    return collectNodes(nodeName, classifier, MergeRule.replace());
}

/**
 * Same as above but uses fixed meta value with given key as identity
 * @param nodeName
 * @param key
 * @return
 */
public Collection<Meta> collectNodes(String nodeName, String key) {
    return collectNodes(nodeName, meta -> getValue(key, Value.NULL));
}

Первый пост в публичном пространстве группы, попробую сделать его коротким (поскольку длинные писать не умею) и полезным. Речь пойдет о механизме dependency injection и переломе моего мозга. Сам шаблон ныне очень популярен особенно в энетерпрайзной среде, но я даже с довольно большим стажем программирования очень не с первого раза въехал в то, что это такое. После прочтения довольно обширной статьи в википедии на ум приходит только:

Я понял - это намёк,
Я всё ловлю на лету
Но непонятно
Что конкретно ты имела в виду

Группа "Несчастный случай"

В смысле, все вроде правильно, но где тут вся суть-то?

Идея такая: предположим у нас есть какой-то объект, которому для работы нужны другие объекты. Скажем, у нас есть транспортный код, естественно для проведения моделирования, нам нужны такие вещи, как геометрический трэккер, генератор случайных чисел и  симулятор взаимодействий. Мы не разбираем случай, когда все это делается на уровне субрутин, и перемешано в одну большую кучу тыщ на 10 строк кода. Так давно уже никто не делает (ну ладно, про никто это я погорячился).

Внедрение или обращение зависимостей вообще имеет смысл только в объектном подходе, так что и говорим о нем, разумеется объектом может быть и функция.

Теперь подумаем, а каким образом наш транспортный код будет генерировать эти вспомогательные объекты.

  1. Навсегда зашитым в коде способом. Если вдруг надо что-то поменять, лезем в код и перекомпиллируем. Если вдруг есть два различных кода для трэккинга, путаемся. Если вдруг вызов кода треккинга происходит не в одном месте, а в двух, трех, пяти и так далее, попадаем в ад той же степени (поменяли ссылку в одном месте и не поменяли в другом счастливо отлавливаем ошибку в течение пары месяцев). В программе больше, чем на пару сотен строк лучше так не делать.
  2. Интерфейсом. Создаем интерфейс, описывающий набор поведений нужного нам класса. Реализуем этот интерфейс одним или несколькими способами (да, часто имеет смысл делать интерфейс с ровно одной реализацией). Далее мы можем получить объект, реализующий этот интерфейс разными способами:
    1. Генерировать его на ходу, используя конфигурацию материнского класса. Это весьма распространенный способ, но главная проблема в том, что при этом надо поддерживать весьма развесистую конфигурационную структуру, в которой легко запутаться. Если вдруг надо добавить для дочернего класса какую-нибудь дополнительную степень свободы, то ее надо будет протаскивать через всю структуру, что довольно затруднительно, если структура сложная.
    2. Определять нужные объекты в конструкторе материнского класса. То есть сначала создать треккер и генератор случайных чисел, а потом из них собрать транспорт. Проблема тут в том, что если таких элементов нужно 10, то конструктор превращается в сущего монстра. В таких случаях довольно успешно используются всяческого рода builder-ы.
    3. Сделать внешний сервис, который будет по запросу нашего материнского класса (транспорта) генерировать нужные элементы (треккер и так далее). При изменении конфигурации мы можем модифицировать только этот сервис, и ничего более.

Собственно последний вариант и принято называть dependency injection. Вообще, разумеется вариант выглядит привлекательно. Дополнительный плюс в том, что так как сервис выдает объект по запросу, то генерация этого объекта, вообще говоря, может быть сделана в тот момент, когда он нужен, что может существенно оптимизировать время запуска программы. Каким образом эту довольно простую идею можно вытащить из того, что написано в википедии, я до сих пор не очень понимаю (особенно учитывая очень странное название).

А теперь к тому, с чего все это началось. Я сижу, изучаю тут код одних товарищей, которые очень любят этот самый dependency injection, и используют его вообще везде. При этом эксплуатируется очень ныне популярная библиотека Guice. Я сперва, читая их код, вообще ничего не понял, поскольку в коде вообще нет никаких запросов к сервису, управляющему зависимостей. Оказывается, guice занимается тем, что при создании объекта, проводит поиск всех полей этого объекта, помеченных специальной аннотацией (в Java и всяческих наследниках, аннотации кода - это своеобразная метапрограмма). После чего, система ищет в себе генератор для объектов того типа, который нужен клиенту и подставляет его. В процессе писания этого поста, я наконец понял, как оно работает и после этого, все кажется довольно изящным, но перед этим мозг был сломан.


Вообще, интерес к dependency injection у меня возник до этого. Дело в том, что при разработке контекстов для DataForge, я сам того не зная, эту концепцию переизобрел. Одна из функций контекста - это как раз "внедрение зависимостей", он тащит в себе набор некоторых сервисов, которые могут быть вызваны любыми клиентами, знающими об этом контексте. Конечно, там еще много всяческих прелестей вроде наследования и динамического переключения, но в смысле архитектуры они вторичны.