Мысли о контекстах в программировании и в Kotlin
Tags: architecture, java, kotlin
Published: 2024-03-16 12:08:24

Последние примерно два года я пользуюсь контекстными ресиверами в своей Kotlin-библиотеке [Kone](https://github.com/lounres/kone), и за это время накопилось некоторое собственное понимание о том, как стоит писать код. Этот текст описывает это видение с небольшими пояснениями, почему я пишу код именно так. Главным приоритетом данной статьи является поиск лучшей структуры нашего кода, читаемость которого уже потом можно исправить синтаксическим сахаром.

## Введение: что за что в ответе?

Парадигма процедурного программирования заключается в том, что мы можем (и нам даже стоит) структурировать наш код в виде функций (*процедур*), каждая из которых отвечает за свою просто поставленную задачу, что позволяет сводить написание функций, отвечающих за большие задачи, к использованию функций (*подпрограмм*), отвечающих за более мелкие задачи (*подзадачи*). Парадигма объектного программирования заключается в том, что мы можем (и опять же нам даже стоит) выделить под каждую задачу свой тип объектов, в котором совмещены структура объектов этого типа (*поля* нашего объекта) и методы взаимодействия с ней (*методы* нашего объекта), что позволяет аналогичным способом конструировать решение больших задач с помощью решений мелких (под)задач. И тут важно заметить одну общую важную черту у обоих подходов: "Выдели **простую** задачу и отвечай **только** за неё."

Однако, есть проблема, заключающаяся в том, что эта договорённость нередко нарушается программистами: всем известна ситуация, когда в мире Java классы делались открытыми, чтобы в них можно было добавить функциональность, которая на деле для них была второстепенна и не являлась частью той задачи, за которую отвечал данный класс. Такое решение обменивало строгую структурированность на синтаксическую простоту (и небольшое улучшение читаемости). Kotlin, например, решает эту проблему с помощью функций-расширений.

## Пишите, как думаете, мыслите, рассуждаете

Первое правило, которое я поставил для себя &mdash; это "пиши, как думаешь". Это банальное правило позволяет контролировать то, что получается: чем ближе Вы пытаетесь приблизить Ваш код к тексту, который его описывает, тем лучше он будет читаться, так как будет подобнее тексту, который можно читать и обдумывать. Также при такой идеологии абсолютно стоит писать промежуточные комментарии, которые описывают действия Вашего кода, так как честный понятный текст без них не обходиться: в нём часто встречаются фразы наподобие "давайте сделаем AAA, чтобы получилось BBB" или "пусть дано CCC, тогда нужно сделать действия DDD".

## Контексты: неявные операции вместо методов

Правило "пиши, как думаешь" вкупе с абстракцией и инкапсуляцией (т.е. с тем самым правилом "каждый тип отвечает за свою простую задачу") образуют следующую мысль: всё, что происходит **между** объектами происходит **между** (!) ними, а значит, что не принадлежит ни одному из них. Но тогда кому оно принадлежит? А оно принадлежит некоторому контексту, в котором Вы мыслите!

Представьте, что вы работаете со списками и хотите сравните парочку из них. Тогда Вы напишете банальный код `list1.equals(list2)`. Но почему-то за равенство отвечает первый список. Почему первый список ответственен за эту функциональность? Почему мы никак не вовлекаем второй список в ответственность за эту функциональность? Получается какая-то неправильная семантика. Становится понятно, что за это должно отвечать что-то внешнее.

Представьте ещё один пример: теперь Вы хотите сравнивать списки как-то иначе. (Например, Вы сравниваете списки строк и хотите игнорировать регистр.) Что Вам пришлось бы сделать? В лучшем случае &mdash; переопределить `equals` у одного из списков, а в худшем &mdash; написать список-обёртку со всеми старыми операциями в неизменном виде и новым `equals`. И всё равно приходится делегировать эту обязанность кому-то одному из списков.

Тут и приходит понимание, что должна быть общая функция в виде `equals(list1, list2)`. Она находится посередине, поэтому она одна и никакой список не в ответе за неё. При этом что делать, если наш "контекст" поменяется и мы захотим другое равенство? Значит должно быть что-то, что в ответе за наше равенство, и что мы можем подменить, чтобы получить новое равенство... Стоп! Так это и значит, что у нашего "контекста" должно быть объектное представление, которое мы можем подменять. Получается, что нам нужно что-то наподобие такого определения:
```java
// Java

public interface ListEquality<E> /* extends Equality<List<E>> */ {
    public boolean equals(List<E> leftList, List<E> rightList2);
}

// ...

ListEquality<String> listEquality = ...;
List<String> list1 = ...;
List<String> list2 = ...;

System.out.println(listEquality.equals(list1, list2));
```

Также и сам контекст может содержать действия над объектами, которые находятся в этом контексте. Например, можно представить распространение звука в разных средах (например, в воздухе и в воде). Особенности распространения звука такие, как скорость распространение звука в том или ином месте, не являются функциями источника звука, так как они опираются на то, в какой среде они находятся. Т.е. эти особенности являются функциями среды, в которой наш источник звука находится. А параметрами этой функции как раз являются источники звука &mdash; т.е. те самые объекты, находящиеся в нашей среде. Таким образом среда является тем самым контекстом.

## Контексты и математика

Особенно приятно такой подход проявляет себя в контексте (извините за каламбур) математических абстракций, так как эти абстракции сами определены как контексты.

Например, в математике есть понятие *кольца*. Это совокупность из множества объектов, которые мы называем "числами", и операций равенства, сложения, вычитания и умножения на этом множестве. Т.е. целые, рациональные или вещественные числа суть кольца. При этом важно заметить, что на том же множестве "чисел" можно иначе определить равенство и арифметические операции и получить новое кольцо. Так, например, есть ещё один нестандартный пример &mdash; остатки по какому-нибудь модулю: их можно, действительно, складывать, вычитать и умножать, а новое равенство "склеивает" воедино числа, дающие один и тот же остаток. Таким образом мы получаем ещё одно кольцо.

Во всех случаях мы можем представить, что числа &mdash; на деле объекты какого-то типа, а операции &mdash; (чистые, "pure") методы контекста. Т.е. кольцо выглядело бы так:
```java
// Java

public interface Ring<N> extends Equality<N> {
    public boolean equals(N left, N right);

    public N plus(N x, N y);
    public N minus(N x, N y);
    public N times(N x, N y);
}

// ...

Ring<Integer> integerRing = ...;
Integer integer1 = ...;
Integer integer2 = ...;

System.out.println(integerRing.minus(integer1, integer2));
```

Да, выглядит последняя строка не очень. Но мы боремся за структурную чистоту. А с синтаксической простой нам поможет синтаксический сахар. Но об этом позже. И поскольку любое математическое рассуждение работает в контексте тех или иных определений и конструкций (например, говоря про вычислительную геометрию, мы предполагаем Евклидово пространство с некоторым набором его свойств и операций в нём), мы можем переложить их в код, завязанный на соответствующие контексты, как есть с минимальными корректировками.

## Контекстные свойства и методы или собственные свойства и методы &mdash; вот в чём вопрос.

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

Примером таких операций для меня были методы `equals`, `hashCode` и `toString` у `Any` в Kotlin. Про `equals` я быстро понял, что понадобились контекстные сравнения. До последнего момента мне казалось, что `hashCode`'у можно остаться собственным методом, но недавно сделал и его контекстным. И потихоньку понимаю, что `toString` тоже стоит заменить на контекст.

Также эта проблема проявлялась во время проектирования новых коллекций. Проблема старых коллекций была в том, что нужна была большая атомарность (т.е. определённый алгоритм хотел предлагать список, который можно расширять, но нельзя менять иными способами). А также способы хранения и относительные свойства (как проверка принадлежности) опирались на захардкоженные равенство и хеширование (т.е. методы `equals` и `hashcode`, где второго, вообще говоря, может не быть). И переопределение таких операций требовало излишних обёрток и некоторую головную боль. Поэтому сначала была принята идея сделать все операции контекстуальными (т.е. каждая операция добавления или удаления и не только принимала ещё и контекст), но это оказалось очень неудобно: конструировать каждый раз контекст очень муторно, а если передать случайно не тот контекст, то внутренняя структура коллекции может сломаться. Так пришло понимание, что структура всё-таки не работает в контексте, а предполагает некоторый контекст, в частности хранит его. Так были сделаны обыкновенные интерфейсы коллекций, чьи реализации при инициализации получали контекст и прятали его внутри себя (т.е. контекст был приватным полем коллекции).

Эта история говорит, что очень важно точно понимать, что за что должно отвечать. При небольшом просчёте весь последующий код мог бы стать излишне накрученным.

Есть также примеры, где я так пока и не смог придумать удобное распределение такой ответственности. И это на самом деле совсем непростая задача.

## Универсальность контекстов

Часто одни контексты можно строить над другим. Например, если у Вас задан контекст равенства на типе `E`, то его можно пропагировать на списки типа `List<E>`:

```java
// Java

public class ListEquality<E> implements Equality<List<E>> {
    Equality<E> elementEquality;
    
    ListEquality(Equality<E> elementEquality) {
        this.elementEquality = elementEquality;
    }
    
    @Override
    public boolean equals(List<E> left, List<E> right) {
        if (left == right) return true;
        if (left.size() != right.size()) return false;
        
        for (int i = 0; i < left.size(); i++) {
            if (!elementEquality.equals(left.get(i), right.get(i))) return false;
        }
        
        return true;
    }
}
```
Тогда построение контекста сравнения списков `Integer`'ов будет просто выглядеть как `ListEquality(IntEquality)`.

Плюс такого подхода &mdash; Вы можете сконструировать (как из блоков Lego) абсолютно полностью контролируемое поведение контекста. Минус &mdash; Вам придётся прописывать это всё руками, и заметно часто такая конструкция становится немаленькой. Последний минус чисто теоретически можно исправить с помощью компиляторного плагина, который будет сам выводить какие-нибудь контексты, отмеченные как дефолтные (как это, например, работает в Rust на trait'ах).

## Наследование контекстов

Ещё приятной особенностью контекстов является возможность их наследования. Таким образом Вы можете передать сложный контекст там, где требуется контекст с определённым свойством. Например, передавать контекст кольца целых чисел в качестве контекста равенства (ведь первый наследует `Ring<Integer>`, который уже наследует `Equality<Integer>`).

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

- Правильно разделите ответственность между несколькими интерфейсами и запрашивайте вместо одного инстанса большого толстого контекста много инстансов атомарных контекстов (если не перебарщивать и найти золотую середину, таких аргументов-контекстов у каждой операции будет не мало и не много).
- Такие инстансы можно потом собирать и хранить вместе в полях одного класса. И при необходимости просто передавать поля этого класса. А если у Вас функциональность между несколькими "хранилищами", то не будет проблемой передавать из них контексты, ведь мы требуем передать не сами хранилища, а их содержимое.

Давайте называть такие контексты "контекстуальными областями" ("context scope"), а сами хранилища &mdash; "контекстами" ("context").

Например, если Вы расширяете кольцо до кольца многочленов, где Вы хотите ввести ещё и операции для многочленов, то создайте новое кольцо-область, отнаследовавшись от `Ring<MyPolynomial>` и дополните его своими операциями (например, умножения числа на многочлен). А потом передавайте аргументами и старое кольцо, и новое. Хранить их при этом Вы можете в специальном хранилище-контексте `MyPolynomialContext`. Если же Вы добавляете операции кольца одновременно для многочленов и рациональных функций, то сделайте отдельную область, расширяющую области кольца чисел и кольца многочленов до области кольца рациональных функций и 
- либо сделайте отдельный контекст для только многочленов и храните его в контексте для рациональных функций (т.е. добавляйте многочлены и рациональные функции не одновременно),
- либо храните в общем контексте одновременно поля для кольца чисел, кольца многочленов и кольца рациональных функций.

В контексте (ещё раз каламбур) Kotlin в обоих случаях Вы можете прокинуть все построенные контексты как контекстные ресиверы одной и той же лямбды, чтобы одной функцией получался доступ ко всем операциям (и внутри одной выделенной области). При этом важно помнить, что если у Вас есть метод, который построен несколько раз в этих контекстах (одна копия &mdash; в одном контексте, другая &mdash; в другом и т.д.), то он вызовет ошибку неоднозначного определения ("overload resolution ambiguity").

## Контексты против рекурсии

Ещё одна крутая особенность контекстного подхода, которую я эмпирически вывел &mdash; это то, что функционал такого подхода может часто заменить функционал других подходов, которые мы сейчас рассмотрим.

### Рекурсивные типы

Есть в Java и Kotlin возможность создавать типы, которые выражаются через себя или друг через друга. Например, так:
```java
// Java

// Тип, выразимый через себя.
public interface DummyInterface1<A extends Comparable<A>> {...}

// Типы, выразимые через друг друга
public interface DummyInterface2<A extends Comparable<B>, B extends Comparable<A>> {...}

// или ещё страшнее
public interface DummyInterfaceX<T extends DummyInterfaceY> {...}
public interface DummyInterfaceY<T extends DummyInterfaceX> {...}
```
Не смотря на то, что такое поведение сложно контролировать, оно ещё и нарушает установленное нами ранее правило "операция между разными сущностями должна быть контекстуальной". Действительно, тут мы просто хотим сказать, что доступны некоторые операции (в данном случае операция сравнения `A.compareTo(A)` или `A.compareTo(B)`вкупе с `B.compareTo(A)`). Поскольку тут указаны только операции между, то они мгновенно заменяются контекстом вида:
```java
// Java

public interface Order<A> {
    public int compare(A left, A right);
}
```
(Да, это очень похоже на `Comparator<A>`. Но мне показалось, что конкретно в Kone стоит сделать свой интерфейс, который можно использовать как контекст.)

Далее этот контекст просто передаётся как дополнительный аргумент туда, где нужно это сравнение. Тут тоже есть два минуса:
1. Из-за контекстов может внезапно сильно увеличиться количество типовых аргументов. Но лично я не считаю такое проблемой.
1. Каждый контекст нужно всегда таскать за собой, как отдельный параметр функции или метода. Но это может исправить синтаксический сахар, которые сам подставляет контекст, где он подразумевается. Мы обсудим это позже.

### Self-типы и наследование статических методов и свойств

Self-типы являются частным случаем рекурсивных типов: они указывают на некоторое свойство, которое завязано на сам тип. Аналогично, наследование статических типов (которых нигде нет, но в принципе довольно желаемы) их можно вместо типа приписать контексту. Таким образом мы можем вывести рекурсивную логику в контекст. Снова.

### Rust-style трейты

Трейты в Rust обладают особым свойством, которое ннедоступно в Java и других языках при прямой интерпретации трейтов как интерфейсов: они могут рекурсивно требовать от своих параметров выполнение такого же трейта. Самый простой пример &mdash; трейт [`Display`](https://doc.rust-lang.org/std/fmt/trait.Display.html).

Казалось бы, такое прекрасно решается интерфейсами. Это, действительно, правда, но трейты в Rust могут быть навешены на тип вне его определения. Т.е., например, на тип, объявленный в некотором крейте (модуле) может быть навешен трейт из зависящего крейта. И такое поведение невозможно для типов: он либо наследует интерфейс сразу, либо никогда.

Но если подумать, то у такой трейт на самом деле является контекстом для ранее объявленного типа. Вы знаете, что я предлагаю дальше :) Да, вынести такое поведение в контекст!

## Минусы

Ну куда уж без минусов! У контекстов в начальной трактовке есть единственный минус: они описывают некоторое динамическое поведение (т.е. поведение, детали которого определяются не статически, а динамически). И этого реально не хватает для полного покрытия Self-типов и Rust-style трейтов.

Другой минус, который мы уже упоминали &mdash; многоссловность. Т.е. вместо особого синтаксиса, который позволяет передавать только один объект мы требуем ещё передачу контекста. Или даже нескольких! Для динамического определения контекста это не так страшно, так как является необходимостью нашего дизайна. Но опять же при статическом определении контекста это получается слишком многословно.

Но рискну и заявлю, что такое можно решить с помощью компиляторных плагинов. (И замечу, что в Kotlin компиляторные плагины являются неотъемлимой частью экосистемы, что очень приятно.)

## Мои ~~сложные~~ нестандартные  примеры 

Далее я показываю, на какие решения я решился.

### Равенство, сравнение и хеширование

#### Контекст равенства

```kotlin
// Kotlin

public interface Equality<in E>: KoneContext {
    public infix fun E.equalsTo(other: E): Boolean = this == other
}

internal object DefaultEquality : Equality<Any?>

public fun <E> defaultEquality(): Equality<E> = DefaultEquality
```

#### Контекст хеширования

```kotlin
// Kotlin

public interface Hashing<in E> : Equality<E> {
    public fun E.hash(): Int = this.hashCode()
}

public inline fun <E> Hashing(crossinline comparator: (left: E, right: E) -> Boolean, crossinline hasher: (E) -> Int): Hashing<E> =
    object : Hashing<E> {
        override fun E.equalsTo(other: E): Boolean = comparator(this, other)
        override fun E.hash(): Int = hasher(this)
    }

internal object DefaultHashing : Hashing<Any?>

public fun <E> defaultHashing(): Hashing<E> = DefaultHashing
```

#### Контекст сравнения (линейного порядка)

```kotlin
// Kotlin

public fun interface Order<in E> : Equality<E> {
    public infix operator fun E.compareTo(other: E): Int
}

context(Order<E>)
public inline infix fun <E> E.greaterThan(other: E): Boolean = this > other
context(Order<E>)
public inline infix fun <E> E.greaterThanOrEqual(other: E): Boolean = this >= other
context(Order<E>)
public inline infix fun <E> E.lessThen(other: E): Boolean = this < other
context(Order<E>)
public inline infix fun <E> E.lessThenOrEqual(other: E): Boolean = this <= other
```

### Коллекции на стероидных контекстах

#### Общие абстракции коллекций

```kotlin
// Kotlin

public interface KoneCollection<out E> {
    public val size: UInt
    public operator fun contains(element: @UnsafeVariance E): Boolean
}

public interface KoneExtendableCollection<E> : KoneCollection<E> {
    public fun add(element: E)
    public fun addAll(elements: KoneIterableCollection<E>) {
        for (e in elements) add(e)
    }
}

public interface KoneRemovableCollection<out E> : KoneCollection<E> {
    public fun remove(element: @UnsafeVariance E)
    public fun removeAllThat(predicate: (element: E) -> Boolean)
    public fun removeAll()
}

public interface KoneMutableCollection<E> : KoneExtendableCollection<E>, KoneRemovableCollection<E>

public interface KoneCollectionWithContext<E, out EC: Equality<E>> : KoneCollection<E> {
    public val elementContext: EC
}

public interface KoneExtendableCollectionWithContext<E, out EC: Equality<E>> : KoneExtendableCollection<E>, KoneCollectionWithContext<E, EC>

public interface KoneRemovableCollectionWithContext<E, out EC: Equality<E>> : KoneRemovableCollection<E>, KoneCollectionWithContext<E, EC>

public interface KoneMutableCollectionWithContext<E, out EC: Equality<E>> : KoneMutableCollection<E>, KoneExtendableCollectionWithContext<E, EC>, KoneRemovableCollectionWithContext<E, EC>
```

#### Списки

```kotlin
// Kotlin

public interface KoneList<out E> : KoneCollection<E> {
    override fun contains(element: @UnsafeVariance E): Boolean

    public operator fun get(index: UInt): E
    public fun getMaybe(index: UInt): Option<E> =
        if (index < size) Some(get(index))
        else None
    public fun indexOf(element: @UnsafeVariance E): UInt
    public fun indexThat(predicate: (index: UInt, element: E) -> Boolean): UInt {
        var i = 0u
        while (i < size) {
            if (predicate(i, get(i))) break
            i++
        }
        return i
    }
    public fun lastIndexOf(element: @UnsafeVariance E): UInt
    public fun lastIndexThat(predicate: (index: UInt, element: E) -> Boolean): UInt {
        var i = size - 1u
        while (i != UInt.MAX_VALUE) {
            if (predicate(i, get(i))) break
            i--
        }
        return i
    }
}

public interface KoneSettableList<E> : KoneList<E> {
    public operator fun set(index: UInt, element: E)
}

public interface KoneExtendableList<E>: KoneList<E>, KoneExtendableCollection<E> {
    override fun add(element: E)
    override fun addAll(elements: KoneIterableCollection<E>)

    public fun addAt(index: UInt, element: E)
    public fun addAllAt(index: UInt, elements: KoneIterableCollection<E>)
}

public interface KoneRemovableList<E>: KoneList<E>, KoneRemovableCollection<E> {
    public fun removeAt(index: UInt)
    public override fun removeAllThat(predicate: (element: E) -> Boolean) {
        removeAllThatIndexed { _, element -> predicate(element) }
    }
    public fun removeAllThatIndexed(predicate: (index: UInt, element: E) -> Boolean)
}

public interface KoneMutableList<E> : KoneSettableList<E>, KoneExtendableList<E>, KoneRemovableList<E>,
    KoneMutableCollection<E>

public interface KoneListWithContext<E, out EC: Equality<E>> : KoneList<E>, KoneCollectionWithContext<E, EC> {
    override fun contains(element: @UnsafeVariance E): Boolean =
        indexThat { _, currentElement -> elementContext { currentElement eq element } } != size

    override fun indexOf(element: @UnsafeVariance E): UInt =
        indexThat { _, currentElement -> elementContext { currentElement eq element } }

    override fun lastIndexOf(element: @UnsafeVariance E): UInt =
        lastIndexThat { _, currentElement -> elementContext { currentElement eq element } }
}
```

#### Мапы

```kotlin
// Kotlin

public interface KoneMap<K, out V> {
    public val size: UInt
    public fun containsKey(key: K): Boolean
    public fun containsValue(value: @UnsafeVariance V): Boolean

    public operator fun get(key: K): V
    public fun getMaybe(key: K): Option<V>

    public val keys: KoneIterableSet<K>
    public val values: KoneIterableCollection<V>
    public val entries: KoneIterableSet<KoneMapEntry<K, V>>
}

public interface KoneMutableMap<K, V>: KoneMap<K, V> {
    public operator fun set(key: K, value: V)
    public fun getAndSet(key: K, value: V): V = getMaybeAndSet(key, value).orThrow { NoMatchingKeyException("There is no value for key $key") }
    public fun getMaybeAndSet(key: K, value: V): Option<V> = getMaybe(key).also { this[key] = value }
    public fun getOrSet(key: K, defaultValue: () -> V): V = getMaybe(key).orElse { defaultValue().also { this[key] = it } }

    public fun remove(key: K)
    public fun getAndRemove(key: K): V = getMaybeAndRemove(key).orThrow { NoMatchingKeyException("There is no value for key $key") }
    public fun getMaybeAndRemove(key: K): Option<V> = getMaybe(key).also { remove(key) }
    public fun removeAllThat(predicate: (key: K, value: V) -> Boolean)

    public fun setAll(from: KoneMap<out K, V>) {
        for ((key, value) in from) set(key, value)
    }
    public fun setAll(from: KoneIterable<KoneMapEntry<K, V>>) {
        for ((key, value) in from) set(key, value)
    }
    public fun removeAll()
}

public interface KoneMapWithContext<K, out KC: Equality<K>, V, out VC: Equality<V>> : KoneMap<K, V> {
    public val keyContext: KC
    public val valueContext: VC
}

public interface KoneMutableMapWithContext<K, out KC: Equality<K>, V, out VC: Equality<V>> : KoneMapWithContext<K, KC, V, VC>, KoneMutableMap<K, V>
```

## Настоящее и будущее контекстов в Kotlin

В данный момент контексты можно передавать только с помощью ресиверов, что не даёт передавать более одного контекста. Хотя часто даже это сильно помогает. Так что контекстный подход пока доступен в ограниченном виде. Но в данный момент разрабатываются способы передавать контексты неявно. Вот что можно глянуть по этому поводу: [KEEP-367](https://github.com/Kotlin/KEEP/issues/367), [KEEP-259](https://github.com/Kotlin/KEEP/issues/259), [KT-10468](https://youtrack.jetbrains.com/issue/KT-10468).

Также по моему примеру с коллекциями видно, что иногда желательно, чтобы коллекция была видна только из одного конкретного контекста (иначе смена контекста может сломать внутреннее устройство нашей коллекции). Получается, что желательно, чтобы контекст не только поставлял функционал, но и отвечал за доступ или за время жизни некоторых объектов. Похожая идея сформировалась у Ильмира Усманова в [заметке KEEP по раскраске кода](https://github.com/Kotlin/KEEP/blob/master/notes/code-coloring.md#limiting-capabilities). Пока для её решения ничего не предполагается. Но, быть может, в будущем мы сможем увидеть некоторую возможность в решении данной проблемы.

## Итог

Подведём итоги нашего рассуждения.

### Паттерны и антипаттерны

Соберём вместе все правила, которые мы выяснили в этом тексте:

1. Пишите так, будто пишите статью с чётким описанием Вашей реализации и подробным описанием того, как и почему она работает.
1. Операции **между несколькими (более чем одним) объектами** не должны принадлежать ни одному из этих объектов, а должны быть вынесены в общий контекст &mdash; контекст определённого вида операций над предоставленными объектами.
1. Не надо резко повышать сложность контекстов. Режьте область необхидмых операций на контекстные области и используйте в таких ситуациях вложение контекстов.
1. Рекурсивные типы, self-типы и Rust-style trait'ы &mdash; ~~зло~~ неправильные подходы. Вместо них можно использовать контексты. Но пока это не самое идеальное решение.
1. Стандартные операции типа `Any` &mdash; зло в случае, когда Вы работаете над часто заменяемыми (например, примитивы) или часто меняющимися (например, коллекции) объектами, и добро в случае, когда Вы работаете над фиксированными структурами (например, те же контексты). Будьте осторожны с тем, что Вы пишите.

### Что ещё можно улучшить?

Не считая стабилизации контекстных ресиверов/параметров, ещё есть смысл написать компиляторный плагин, который позволяет, например, определять аннотациями контексты, привязанные к типу (их же можно назвать "контекстами по-умолчанию"). Но, к сожалению, это пока не такая простая задача. И мы просто рассуждаем, что гипотетически могло бы улучшить нашу жизнь.