Upgrade to Pro — share decks privately, control downloads, hide ads and more …

[SnowOne 2025] Александр Нозик: Мысли в контексте!

[SnowOne 2025] Александр Нозик: Мысли в контексте!

Расскажу, что такое контекстно-ориентированное программирование, как оно работает (да!) в разных языках. Разумеется, обсудим контекстные ресиверы и контекстные параметры в Kotlin (параметры выходят в 2.2) и зачем они нужны.

Видео: https://youtu.be/t_xi9dZ_Bt8

Avatar for jugnsk

jugnsk

May 07, 2025
Tweet

More Decks by jugnsk

Other Decks in Programming

Transcript

  1. Обо мне • Директор Центра научного программирования. • К. ф.–м.

    н. по физике частиц. • Преподаватель МФТИ. • Руководитель российского KUG. • https://sciprog.center/people/Nozik • https://twitter.com/noraltavir • https://t.me/noraltavir 2
  2. Контекст КОП • https://dl.acm.org/doi/10.1145/940923.940926, 2003, Roger Keays, Andry Rakotonirainy •

    https://www.jot.fm/issues/issue_2008_03/article4/ 2008, Robert Hirschfeld, Pascal Costanza, Oscar Nierstrasz. • https://proandroiddev.com/an-introduction-context-oriented-programming-in-kotlin- 2e79d316b0a2, 2018, Alexander Nozik 4 … Тайп- классы в Haskell 2003 Первое упоминание КОП 2008 Реализация КОП для Java и SmallTalk 2010 Улучшенные имплиситы в Scala 2016 Kotlin! 2018 статья про КОП в Kotlin 2022 Первая версия контекстных ресиверов 2025 Контекстные параметры
  3. // -[1/2]- Direct layer activation with code replication if (currentUser().verboseMode())

    with(Layers.Adress).eval(new Block() { public void eval() { for (Person p : personList) System.out.println(p); } }); else for (Person p : personList) System.out.println(p); // -[3]- Direct layer activation without code replication with(currentUser().verboseMode() ? new Layer[]{Layers.Adress} : new Layer[]{}).eval(new Block() { public void eval() { for (Person p : personList) System.out.println(p); } }); // -[4]- Direct layer activation based on context object with(context.computeLayers()).eval(new Block() { public void eval() { for (Person p : PersonList) System.out.println(p); } }); Контекст КОП https://www.jot.fm/issues/issue_2008_03/article4/ 2008, Robert Hirschfeld Захватываем контекст (через ThreadLocal) Добавляем контекст 5
  4. Определение (в контексте доклада) Возникновение и изменение поведений объектов в

    зависимости от того, в каком окружении они вызваны. Работа с лексическими скоупами как со структурными единицами кода. 6
  5. Определение (в контексте доклада) 7 package center.sciprog.scope import … class

    ScopeClass { fun withScope(block: ReceiverType.() -> Unit) { with(ReceiverType){ … block() } } fun doInScope(){ withScope { … } } } Скоуп файла/модуля компилляции Скоуп класса/типа Скоуп функции Скоуп ресивера
  6. В других контекстах - Scala https://docs.scala-lang.org/tour/implicit-parameters.html trait Comparator[A]: def compare(x:

    A, y: A): Int object Comparator: given Comparator[Int] with def compare(x: Int, y: Int): Int = Integer.compare(x, y) given Comparator[String] with def compare(x: String, y: String): Int = x.compareTo(y) end Comparator def max[A](x: A, y: A)(using comparator: Comparator[A]): A = if comparator.compare(x, y) >= 0 then x else y println(max(10, 6)) // 10 println(max("hello", "world")) // world Объявление интерфейса Реализация для конкретного типа Функция захватывает параметр (неявно) из контекста Неявное связывание параметра по типу 8 Пропустили Haskell
  7. В других контекстах - Java try (Arena arena = Arena.ofConfined())

    { // Allocate off-heap memory and // copy the argument, a Java string, into off-heap memory MemorySegment nativeString = arena.allocateFrom(s); // Obtain an instance of the native linker Linker linker = Linker.nativeLinker(); // Locate the address of the C function signature SymbolLookup stdLib = linker.defaultLookup(); MemorySegment strlen_addr = stdLib.find("strlen").get(); // Create a description of the C function FunctionDescriptor strlen_sig = FunctionDescriptor.of( ValueLayout.JAVA_LONG, ValueLayout.ADDRESS ); // Create a downcall handle for the C function MethodHandle strlen = linker.downcallHandle(strlen_addr, strlen_sig); // Call the C function directly from Java return (long)strlen.invokeExact(nativeString); } https://docs.oracle.com/en/java/javase/22/core/ calling-c-library-function-foreign-function-and- memory-api.html 10
  8. В других контекстах - Spring // use constructor-injection to supply

    the PlatformTransactionManager class SimpleService(transactionManager: PlatformTransactionManager) : Service { // single TransactionTemplate shared amongst all methods in this instance private val transactionTemplate = TransactionTemplate(transactionManager) fun someServiceMethod() = transactionTemplate.execute<Any?> { updateOperation1() resultOfUpdateOperation2() } } https://docs.spring.io/spring-framework/reference/data-access/transaction/programmatic.html Контекст DI Spring Контекст транзакции Автоматическое закрытие транзакции 11
  9. Aspect vs Context AOP • Определяем точки «врезки», в которых

    мы можем что-то контролировать. • Вставляем код, который делает что нам надо. • Контроль со стороны аспекта. COP • Определяем, что нам нужно из контекста. • Запрашиваем это у контекста если надо. • Контроль со стороны логики программы. 12 https://www.spiceworks.com/tech/devops/articles/what-is-aop/
  10. Контекст на ровном месте class A{ fun doSomething(){ println("This method

    is called on $this") } } class A{ fun doASomething(){ val b = object: B{ override fun doBSomething(){ println( "This method is called on $this inside ${this@A}" ) } } b.doBSomething() } } Как бы контекст Внешний контекст (диспатчеризуется динамически) Можно сделать интерфейсом чтобы можно было делать абстракции 14
  11. Расширения в Kotlin class A fun A.doASomthing() { println("This extension

    method is called on $this") } val a = A() a.doASomthing() Явный абстрактный контекст (статическая диспатчеризация) Явное задание контекста 15
  12. Here comes the feeling scope class A { fun doInternalSomething()

    {} } fun A.doASomthing() {} val a = A() with(a) { doInternalSomething() doASomthing() } Входим в скоуп, связанный с типом A Вызываем метод класса в скоупе. Диспатчеризуется динамически. Точно также используем в скоупе. Диспатчеризуется статически. 16
  13. Member extension врывается в чат class B class A{ fun

    B.doBSomething(){} } val a = A() val b = B() with(a){ b.doBSomething() // this will work } b.doBSomething() // won't compile Тут a – dispatch receiver, b – extension receiver. Больше! Больше ресиверов! 17
  14. Extension oriented design • Вместо наследования, добавляем методы статически в

    скоуп, порожденный типом. • Задаем контекст разрешения расширений при помощи импорта. 18
  15. Немного математики val n: Number = 1.0 n + 1.0

    // the `plus` operation is not defined on `Number` 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 {…} val n1: Number = 1.0 val n2: Number = 2 val res = with(DoubleOperations){ (n1 + n2)/2 } println(res) 19 Определяем тип контекста Конкретная реализация Вход в контекст. Контекст может быть абстрактным или конкретным.
  16. Немного математики fun NumberOperations.calculate(n1: Number, n2: Number) = (n1 +

    n2)/2 val resWithDoubles = DoubleOperations.calculate(n1, n2) val resWithInts = IntOperations.calculate(n1, n2) val resWithComplex = ComplexOperations.calculate(n1, n2) 20 Декларация функции в абстрактном контексте Вычисление в конкретном контексте
  17. Контекст корутины fun CoroutineScope.doSomeWork() fun CoroutineScope.launch(block: suspend CoroutineScope.() -> Unit):

    Job GlobalScope.launch { launch { val job = coroutineContext[Job] delay(100) doSomeWork() } } 21 Объявляем функцию, которая выполняется в абстрактном скоупе Входим в конкретный скоуп Создаем новый скоуп на основе родительского Вызываем нашу функцию в скоупе Явным образом используем контекст Корутин-билдер из библиотеки
  18. @Composable public fun MultiSelectChooser(…) { var expanded by remember {…}

    var selected: Set<Value> by remember(value) {…} ExposedDropdownMenuBox( expanded = expanded, onExpandedChange = { expanded = !expanded } ) { //this: ExposedDropdownMenuBoxScope ExposedDropdownMenu( expanded = expanded, onDismissRequest = { expanded = false } ) { descriptor?.allowedValues?.forEach { item: Value -> DropdownMenuItem(…) { if (item in selected) {…} else {…} } } } } } Контекст UI 22 Вход в скоуп компонента Объявление изменяемого состояния в скоупе Объявление внутреннего скоупа и захват (изменяемого) состояния Явное обозначение типа скоупа Использование состояния, захваченного из разных внешних скоупов
  19. Еще примеры • https://medium.com/proandroiddev/diving-deeper-into-context- oriented-programming-in-kotlin-3ecb4ec38814 - статья с дополнительными примерами.

    • https://github.com/Kotlin/KEEP/issues/367 - дискуссия KEEP. • https://youtrack.jetbrains.com/issue/KT-10468 - дискуссия в YouTrack. 23
  20. Keep-87: Имплиситы. Фантомная угроза • Душа просит тайп-классов • Тайп-класс

    это набор расширений, который привязывается к типу неявно. typeclass Monoid { fun combine(b: Self): Self fun Self.Companion.empty(): Self } extension Int : Monoid { fun combine(b: Int): Int = this + b fun Int.Companion.empty(): Int = 0 } 1.combine(2) // 3 Int.empty() // 0 От авторов https://47degrees.github.io 26
  21. Keep-87: Имплиситы наносят ответный удар extension object UserRepository : Repository<User>

    { val storedUsers: MutableMap<Int, User> = mutableMapOf() // e.g. users stored in a DB override fun loadById(id: Int): User? { return storedUsers[id] } override fun User.save() { storedUsers[this.id] = this } } fun <A> fetchById(id: Int, with repository: Repository<A>): A? { return loadById(id) // Repository syntax is automatically activated inside the function scope! } От авторов https://47degrees.github.io 27
  22. Keep-176 fun [A, B, C].someMethod(v: Int): Int = ... val

    a: [A, B, C].(Int)->Int = ... https://github.com/Kotlin/KEEP/pull/176 Передаем список ресиверов вместо одного. Нет видимой разницы между context receiver и dispatch receiver object Intersection: A, B, C with(Intersection){ doSomething() } with(a, b, c){ doSomething() } Эмуляция пересечения типов 28
  23. KEEP 87 vs KEEP 176 KEEP-87 • Контекст привязывается к

    типу. • Связывание неявное, то есть если есть нужное расширение, оно подключается само. Можно указать контекст явно (но это опционально). KEEP-176 • Контекст должен быть указан явно в точке вызова функции. • Связывание явное. Контекстов может быть много и они могут пересекаться. 29 Контекст распространяется на все, что находится внутри блока, связавшего его.
  24. context(Comparator<T>) infix operator fun <T> T.compareTo(other: T) = compare(this, other)

    context(Comparator<T>) val <T> Pair<T, T>.max get() = if (first > second) first else second with(compareBy<Double> { it.rem(1.0) }){ (1.2 to 2.1).max } KEEP-259 в Kotlin 1.7 (context receivers) https://github.com/Kotlin/KEEP/blob/master/proposals/context-receivers.md Параметрический модификатор Использование контекстного ресивера Вход в контекст 31 1.2
  25. KEEP-259 в Kotlin 1.7 (context receivers) Почему такой синтаксис? https://github.com/Kotlin/KEEP/blob/master/proposals/context-receivers.md

    Должна быть симметрия между вводом декларацией и вызовом фичи. 32
  26. KEEP-259 в Kotlin 1.9 (context receivers) До контекстных ресиверов public

    interface RingOps<T> : GroupOps<T> { public fun multiply(left: T, right: T): T public operator fun T.times(arg: T): T = multiply(this, arg) } После контекстных ресиверов https://github.com/Kotlin/KEEP/blob/master/proposals/context-receivers.md public interface RingOps<T> : GroupOps<T> { public fun multiply(left: T, right: T): T } context(RingOps<T>) operator fun <T> T.times(other: T): T = multiply(this, other) context(BufferRingOps<T, *>) operator fun <T> Point<T>.times(other: T): Point<T> = with(elementAlgebra) { Point(size) { get(it) * other } } 33
  27. KEEP-259 в Kotlin 1.7 (context receivers) page(route) { … div("inner")

    { h2 { +"Команда" } div("features") { team.forEach { member -> section { a { val imagePath = member.imagePath?.let { resolveRef(it) } img( classes = "icon major", src = imagePath, alt = imagePath ) { h3 { +member.name } fragment(member) } } } } } } } https://github.com/Kotlin/KEEP/blob/master/proposals/context-receivers.md context(PageContextWithData, FlowContent) public fun fragment(data: Data<PageFragment>) Точка вставки Контекст страницы 34 public fun DataSink<Any>.page( name: Name, pageMeta: Meta = Meta.EMPTY, block: context(PageContextWithData) HTML.() -> Unit, )
  28. KEEP-259 в Kotlin 1.7 (context receivers) Проблемы: • Scope pollution

    • Ошибки разрешения/пропагации скоупа https://github.com/Kotlin/KEEP/blob/master/proposals/context-receivers.md class AClass { // this: AClass fun Extension.doSomething() { // this: Extension dsl1 { // this: Builder1 dsl2 { // this: Builder2 foo() // where is this function declared? } } } } https://github.com/Kotlin/KEEP/blob/master/ proposals/context-receivers.md#context- receivers-abuse-and-scope-pollution 35
  29. KEEP-367 в Kotlin 2.2 (context parameters) context(logger: Logger) fun logWithTime(message:

    String) = logger.log("${LocalDateTime.now()}: $message") context(logger: Logger) fun User.doAction() { logWithTime("saving user $id") // ... } https://github.com/Kotlin/KEEP/blob/master/proposals/context-parameters.md context(Logger) fun logWithTime(message: String) = log("${LocalDateTime.now()}: $message") context(Logger) fun User.doAction() { logWithTime("saving user $id") // ... } KEEP-259 KEEP-367 36
  30. KEEP-367 в Kotlin 2.2 (context parameters) Summary of changes from

    the previous proposal • Introduction of named context parameters, • Context receivers are dropped, • Removal of this@Type syntax, introduction of implicit<A>(), • Contexts are not allowed in constructors, • Callable references resolve their context arguments eagerly, • Context-in-classes are dropped. 37
  31. As implicits context(users: UserRepository) fun User.getFriends() = ... • Явный

    захват переменных из контекста. • Статическое разрешение контекста (можно узнать какой конкретный тип будет во время компиляции). 39
  32. As scopes // currently uses extension receivers fun ResourceScope.openFile(file: File):

    InputStream // but the API is nicer using context parameters context(_: ResourceScope) fun File.open(): InputStream • Добавление поведений объектам в определенных условиях • Ограничение области видимости • Ограничение области жизни 40
  33. Скоуп и контекст как гражданин Всегда было: • Лексический скоуп

    имеет свой контекст. • Скоуп захватывает внешний скоуп, таким образом присутсвует неявное наследование контекста. • Скоуп может порождаться типом. Можно с контекстными параметрами: • Скоуп может наследоваться явно. • Поведения добавляются в скоуп после его объявления. 41
  34. Скоуп и контекст как гражданин 42 context(comparator: Comparator<T>) operator fun

    <T> T.compareTo(other: T): Int = comparator.compare(this, other) context(comparator: Comparator<T>) operator fun <T> List<T>.compareTo(other: List<T>): Int { … } object AlphaNumericComparator: Comparator<String> { override fun compare(o1: String?, o2: String?): Int {…} } with(AlphaNumericComparator) { listOf<String>("2","1","0") > listOf<String>("2", "1", "20", "Beta2") }
  35. Ограничение видимости 43 object MarkerScope context(_: MarkerScope) fun MutableList<String>.add(number: Number)

    with(MarkerScope){ listOf("a").add(2) } Маркерный тип (не используется в рантайме) Поведение добавляется только в определенном скоупе.
  36. Ограничение времени жизни 44 context(_: ResourceScope) fun File.open(): InputStream ResourceScope.doInScope

    { val aStream = File("a.txt").open().bufferedReader() val bStream = File("b.txt").open().bufferedReader() aStream.lineSequence().zip(bStream.lineSequence()) { aLine, bLine -> TODO("zip lines into something") } } Ресурсы автоматически закрываются и точно не выходят за рамки скоупа
  37. div("col-6") { vision { plotly { layout {…} trace {…}

    } } } For extending DSLs 45 context(rootConsumer: VisionTagConsumer<*>) public fun TagConsumer<*>.plot( config: PlotlyConfig = PlotlyConfig(), block: Plot.() -> Unit, ): Unit = with(rootConsumer) { vision { plotly(config, block) } }
  38. Context-oriented dispatch 46 public interface GroupOps<T> : Algebra<T> { public

    fun add(left: T, right: T): T public fun negate(value: T): T } context(_: GroupOps<T>) public operator fun <T> T.unaryPlus(): T context(ops: GroupOps<T>) public operator fun <T> T.unaryMinus(): T = ops.negate(this) context(ops: GroupOps<T>) public operator fun <T> T.plus(arg: T): T = ops.add(this, arg) context(ops: GroupOps<T>) public operator fun <T> T.minus(arg: T): T = ops.add(this, ops.negate(arg)) context(ops: GroupOps<T>) public fun <T> Collection<T>.sum(): T = reduce(ops::add) public interface GroupOps<T> : Algebra<T> { public fun add(left: T, right: T): T public operator fun T.unaryMinus(): T public operator fun T.unaryPlus(): T = this public operator fun T.plus(arg: T): T = add(this, arg) public operator fun T.minus(arg: T): T = add(this, -arg) } Может определяться в месте использования
  39. Dependency injection 47 interface Logger { ... } interface UserService

    { ... } class DbUserService( val logger: Logger, val connection: DbConnection ): UserService { ... } context(logger: Logger, connection: DbConnection) fun DbUserService(): DbUserService = DbUserService(logger, connection) context(logger, userService){ ... context(newLogger){ val dbUserService = DbUserService() } } Нормальные параметры в конструкторе Инжектируем параметры из контекста в конструктор Явная передача контекста Смена контекста
  40. Выводы • Контекстно-ориентированное программирование как идея существует давно. • Отдельные

    аспекты есть в разных языках, но реализация не очень красивая. • Контекст очень близко связан с понятием лексического скоупа. • В Kotlin «от рождения» есть поддержка КОП. Но не хватало возможности явно формализовать захват множества контекстов. • Теперь можно! Начиная с 2.2.0 2.1.20, все работает в мультиплатформе. • Эта фича может радикально поменять дизайн библиотек. Особенно в смысле работы с ресурсами. 49
  41. context[references] Статьи тут: • https://proandroiddev.com/an-introduction-context-oriented-programming-in-kotlin-2e79d316b0a2 • https://proandroiddev.com/diving-deeper-into-context-oriented-programming-in-kotlin-3ecb4ec38814 Обсуждения тут: •

    https://youtrack.jetbrains.com/issue/KT-10468 • https://github.com/Kotlin/KEEP/pull/176 • https://github.com/Kotlin/KEEP/pull/87 Текущее (предрелизное) состояние тут: • https://github.com/Kotlin/KEEP/blob/master/proposals/context-parameters.md • https://github.com/Kotlin/KEEP/issues/367 Комьюнити тут: • https://t.me/kotlin_lang • https://t.me/kotlin_russia 50