From bd54549b452723b790f2b3db0590443a4f61818c Mon Sep 17 00:00:00 2001 From: Alejandro Serrano Date: Thu, 21 Dec 2023 16:43:14 +0100 Subject: [PATCH 01/28] Context parameters --- proposals/context-parameters.md | 560 ++++++++++++++++++++++++++++++++ 1 file changed, 560 insertions(+) create mode 100644 proposals/context-parameters.md diff --git a/proposals/context-parameters.md b/proposals/context-parameters.md new file mode 100644 index 000000000..8e2bf59eb --- /dev/null +++ b/proposals/context-parameters.md @@ -0,0 +1,560 @@ +# Context parameters + +* **Type**: Design proposal +* **Author**: Alejandro Serrano +* **Contributors**: Marat Ahkin, Nikita Bobko, Ilya Gorbunov, Mikhail Zarechenskii, Denis Zharkov +* **Discussion**: [KEEP-?](https://github.com/Kotlin/KEEP/issues/?) + +## Abstract + +This is an updated proposal for [KEEP-259](https://github.com/Kotlin/KEEP/issues/259), formerly known as _context receivers_. The new design addresses the issues raised by the users of the prototype and across the community. + +This document is not (yet) formally a KEEP, since it lacks some of the technical elements. Those are going to be provided at a later time, but we thought it would be interesting to open the discussion even if the design is not fully formalized. + +### Summary of changes from [previous proposal](https://github.com/Kotlin/KEEP/issues/259) + +1. Introduction of named context parameters, +2. Removal of `this@Type` syntax, introduction of `summon()`, +3. Not every member of a context receiver is accessible, only those with a context, +4. No subtyping check between different context parameters, +5. Contexts are not allowed in constructors, +6. Callable references resolve their context arguments eagerly, +7. Context-in-classes are dropped. + +## Table of contents + +* [Abstract](#abstract) + * [Summary of changes from previous proposal](#summary-of-changes-from-previous-proposal) +* [Table of contents](#table-of-contents) +* [Members with context parameters](#members-with-context-parameters) +* [Use cases](#use-cases) + * [As implicits](#as-implicits) + * [As scopes](#as-scopes) + * [For extending DSLs](#for-extending-dsls) + * [Context-oriented dispatch / externally-implemented interface / type classes](#context-oriented-dispatch--externally-implemented-interface--type-classes) + * [Dependency injection](#dependency-injection) +* [Standard library support](#standard-library-support) + * [Reflection](#reflection) +* [Context receivers](#context-receivers) + * [⚠️ Preliminary warning](#️-preliminary-warning) + * [Context receivers and scope](#context-receivers-and-scope) + * [The migration story for receivers](#the-migration-story-for-receivers) +* [Callable references](#callable-references) +* [Context and classes](#context-and-classes) +* [Technical design](#technical-design) + * [Syntax](#syntax) + * [Extended resolution algorithm](#extended-resolution-algorithm) + * [JVM ABI and Java compatibility](#jvm-abi-and-java-compatibility) + * [Alternative scoping](#alternative-scoping) +* [Q\&A about design decisions](#qa-about-design-decisions) +* [Acknowledgements](#acknowledgements) + + +## Members with context parameters + +**§1** *(context parameters)*: Every callable member (functions — but not constructors — and properties) gets additional support for **context parameters**. Context parameters are declared with the `context` keyword followed by a list in parentheses. There are two kinds of context parameters: + +* **Named context parameters:** with a name, using `name: Type` syntax. +* **Context receivers:** without a name, simply listing the type. + +These mirror the two kinds of parameters we can currently declare in Kotlin: value parameters, and receivers. The difference is that they are **implicitly** passed using other context arguments. + +```kotlin +context(logger: Logger) fun User.doAction() { ... } +``` + +**§2** *(context parameters, restrictions)*: + +* It is an *error* if the **name** of a context parameter **coincides** with the name of another context or value parameter to the callable. +* We want to warn the user (warning, inspection) if there are two context receivers for which one is a **supertype** of (or equals to) the other, since in that case the shared members would always result in an ambiguity error. + * This was an error in the previous iteration of the design. + +**§3** *(context parameters, order)*: Named context parameters and context receivers may be freely interleaved in a `context` declaration. + +* Although people have already referred to some style guidelines, we prefer those to arise organically from the community. + +**§4** *(empty context)*: The context may be empty, in which case we can write either `context()` or drop the parentheses to get `context`. + +* This affects contextual visibility, as explained below. +* It is useless to declare an empty context in a function without an extension or dispatch receiver, so that case may warrant a warning. +* It is implementation-dependent whether it is allowed to declare two callables with the same signature, except for an empty context in one of them. + * In the case of Kotlin/JVM, this results in a platform clash unless one of them is renamed. + +**§5** *(override)*: Context parameters are part of the signature, and follow the same rules concerning overriding: + +* When overriding, the type and order of context parameters must coincide. + * Even if the context is empty, you cannot override a function without context with one with it, or vice versa. +* It is allowed (yet discouraged) to change the name of a context parameter or its receiver/named status. + +**§6** *(naming ambiguity)*: We use the term **context** with two meanings: + +1. For a declaration, it refers to the collection of context parameters declared in its signature. We also use the term *contextual function or property*. +2. Within a body, we use context to refer to the combination of the implicit scope, context receivers, and context parameters. This context is the source for context resolution, that is, for "filling in" the implicit context parameters. + +**§7** *(function types)*: **Function types** are extended with context parameters, following the same syntax as we have for declarations. However, we do **not** support named context parameters in function types. + +* That way we don't have to inspect the body of a lambda because of this. The reasoning is similar to how we restrict lambdas with no declared arguments to be 0 or 1-ary. + +```kotlin +context(Transaction) (UserId) -> User? +context(Logger) User.() -> Int +``` + +Note that, like in the case of regular receivers, those types are considered equivalent (for typing purposes, **not** for resolution purposes) to the function types in which all parameters are declared as "regular" ones. + +## Use cases + +This is a recollection and categorization of the different use cases we have found for context parameters, including guidelines on which kind of parameter is more applicable (if so). + +### As implicits + +**§A** *(implicit use case)*: In this case, the context parameter is thought of as a set of services available to a piece of code, but without the ceremony of passing those services explicitly in every call. + +* In most cases, those contexts are introduced with a name. +* The context parameter type may or may not have been designed to appear as context. + * For example, it comes from a Java library. + +A `Repository` class for a particular entity or a `Logger` are good examples of this mode of use. + +```kotlin +context(users: UserRepository) fun User.getFriends() = ... +``` + +### As scopes + +**§B** *(scope use case)*: In this case we use the context parameter as a marker of being inside a particular scope, which unlocks additional abilities. A prime example is `CoroutineScope`, which adds `launch`, `async`, and so on. In this mode of use: + +* The `Scope` type is carefully designed and the functions marked as `context`, +* When used in a function, it appears as context receiver. + +This covers other types of scopes as `Transaction`, or `Resource`-related. The `Raise` and `ResourceScope` DSLs from the Arrow project also fit this view. + +```kotlin +// currently uses extension receivers +fun ResourceScope.openFile(file: File): InputStream +// but the API is nicer using context receivers +context(ResourceScope) fun File.open(): InputStream +``` + +We describe later in this document how library authors should ponder context parameters into their existing design. + +### For extending DSLs + +**§C** *(DSLs use case)*: In this case, contexts are used to provide new members available in a domain-specific language. Currently, this is approached by declaring an interface which represents the "DSL context", and then have member or extension functions on that interface. + +```kotlin +interface HtmlScope { + fun body(block: HtmlScope.() -> Unit) +} +``` + +Context parameters lift two of the main restrictions of this mode of use: + +* It's possible to add new members with extension receiver without modifying the Scope class itself. +* It's possible to add members which are only available when the DSL Scope has certain type arguments. + +### Context-oriented dispatch / externally-implemented interface / type classes + +**§D** *(Context-oriented dispatch use case)*: Context receivers can be used to simulate functions available for a type, by requiring an interface that uses the type as an argument. This is very similar to type classes in other languages. + +```kotlin +interface Comparator { + context fun T.compareTo(other: T): Boolean +} + +context(Comparator) fun max(x: T, y: T) = + if (x.compareTo(y) > 0) x else y +``` + +### Dependency injection + +**§E.1** *(Dependency injection use case)*: You can view context parameters as values that must be "injected" from the context for the code to work. Since context arguments are completely resolved at compile-time, this provides something like dependency injection in the language. + +**§E.1** *(Dependency injection use case, companion object)*: In some cases, you may want to define that instantiating a certain class requires some value — this creates the typical hierarchy of dependency we see in DI frameworks. We can accomplish this by faking a constructor with those contexts: + +```kotlin +interface Logger { ... } +interface UserService { ... } + +class DbUserService(val logger: Logger, val connection: DbConnection) { + companion object { + context(logger: Logger, connection: Connection) + operator fun invoke(): DbUserService = DbUserService(logger, connection) + } +} +``` + +**§E.2** *(Dependency injection use case, discouragement)*: Note that we suggest against this feature, since having parameters explicitly reduces the need to nest too many `context` calls. + +```kotlin +// do not do this +context(ConsoleLogger(), ConnectionPool(2)) { + context(DbUserService()) { + ... + } +} + +// better be explicit about object creation +val logger = ConsoleLogger() +val pool = ConnectionPool(2) +val userService = DbUserService(logger, pool) +// and then inject everything you need in one go +context(logger, userService) { + ... +} +``` + +## Standard library support + +**§8** *(`context` function)*: To extend the implicit scope in a contextual manner we provide additional functions in the standard library. + +* The implementation may be built into the compiler, instead of having a plethora of functions defined in the standard library. + +```kotlin +fun context(context: A, block: context(A) () -> R): R = block(context) +fun context(a: A, b: B, block: context(A, B) () -> R): R = block(a, b) +fun context(a: A, b: B, c: C, block: context(A, B, C) () -> R): R = block(a, b, c) +``` + +**§9** *(`summon` function)*: We also provide a generic way to obtain a value by type from the context, + +```kotlin +context(ctx: A) fun summon(): A = ctx +``` + +This function replaces the uses of `this@Type` in the previous iteration of the design. + +### Reflection + +**§10** *(callable reflection)*: The following additions to the `kotlin.reflect` are required for information about members. + +```kotlin +interface KParameter { + enum class Kind { + INSTANCE, EXTENSION_RECEIVER, VALUE, + **NAMED_CONTEXT****,** **CONTEXT_RECEIVER** + } +} + +val KCallable<*>.declaresContext: Boolean + +val KCallable<*>.contextParameters: List + get() = parameters.filter { + it.kind == KParameter.Kind.NAMED_CONTEXT || it.kind == KParameter.Kind.CONTEXT_RECEIVER + } + +// +``` + +It is possible that `(c.declaredContext && c.contextParameters.isEmpty())` is true if the callable declares an empty context + +**§11** *(property reflection)*: Properties with context receivers are not `KProperty0`, `1`, nor `2`, but rather simply `KProperty`. + +## Context receivers + +### ⚠️ Preliminary warning + +**§12** *(context receivers, warning)*: Although the previous iteration of this design was called *context receivers*, and this document spends most of the time speaking about them because they pose a greater design challenge, those are *not* the main use mode of context parameters. + +From a documentation (and educational) perspective, we emphasize that *named context parameters* solve many of the problems related to implicitly passing information around, without the caveat of scope pollution. + +```kotlin +context(scope: CoroutineScope) fun User.doSomething() = scope.launch { ... } +``` + +Context receivers have a narrower use case of delimiting a *context* and making some additional functions only available *within* them. For technical reasons, context receivers (and not named context parameters) also appear in function types, but those types are not usually written by the developers themselves. + +### Context receivers and scope + +**§13** *(context receivers, motivation)*: we want context receivers to expose a carefully designed API, so the design takes this particular mode of usage into account. For that reason, we ask library designers to think about which members are available when their types are used as context receivers. + +**§14** *(contextual visibility)*: Concretely, whenever a context receiver is brought into scope, only the members with a declared context are available without qualification. Note that this includes those with an 0-ary context, which can be thought of as a "marker" for contextual visibility. + +```kotlin +interface Logger { + context fun log(message: String) // context() fun log(..) + fun thing() +} + +fun Logger.shout(message: String) = log(message.uppercase()) + +context fun Logger.whisper(message: String) = log(message.lowercase()) + +context(Logger) fun logWithName(name: String, message: String) = + log("$name: $message") + +context(Logger) fun doSomething() { + log("Hello") // ok + thing() // unresolved reference 'thing' + summon().thing() // ok + shout("Hello") // unresolved reference 'shout' + whisper("Hello") // ok + logWithName("Alex", "Hello") // ok +} + +fun somethingElse() { + log("Hello") // unresolved reference 'log' + logWithName("Alex", "Hello") // no context receiver for 'Logger' found +} +``` + +### The migration story for receivers + +**§15** *(receiver migration, caveats)*: It is **not** desirable to change existing APIs from extension receivers to context parameters, because of the potential changes in resolution (different meaning of `this`, changes in callable references). This is especially relevant for libraries which are consumed by virtually everybody (standard library, coroutines). + +**§16** *(receiver migration, scope types)*: If your library exposes a "scope" or "context" type, we suggest adding an empty context to their members as soon as possible. Every extension function over this type also needs that modifier. + +*How do you know that your type `S` is of such kind?* + +* It's often used as extension receiver, but without being the "subject" of the action. +* There's some "run" function that takes a `S.() → R` function as an argument. + + +The benefit for the consumer of the library is that now they can write functions where the type is a context receiver. This can lead to nicer-to-read code, since it "frees the extension position". + +```kotlin +context(ApplicationContext) fun User.buildHtml() = ... +``` + +**§17** *(receiver migration, functions with subject)*: We encourage library authors to review their API, look for those cases, and provide new variants based on context parameters, instead of merely marking the original function with context. + +```kotlin +// Uses extension receivers, but File is in fact the "subject" +fun ResourceScope.openFile(file: File): InputStream + +// Doing this is OK, but provides no additonal benefit +context fun ResourceScope.openFile(file: File): InputStream + +// The API is nicer using context receivers +@JvmName("contextualFileOpen") // needed to avoid platform clash +context(ResourceScope) fun File.open(): InputStream +``` + +**§18** *(receiver migration, alternatives)*: Our design strongly emphasizes that library authors are key in exposing their API using context receivers. As we discussed above, named context parameters should be first considered. However, if you really want this ability for types you do not control, there are two possibilities. + +* Named context + `with` + + ```kotlin + context(ctx: ApplicationContext) fun User.buildHtml() = with(ctx) { ... } + ``` + +* Lightweight wrapper type that exposes the functionality as contextual + + ```kotlin + // if it's an interface, you can delegate + @JvmInline value class ApplicationContext( + val app: Application + ) { + context fun get() = app.get() + // copy of every function, but with context on front + } + + // you can also use it to limit the available methods + @JvmInline value class MapBuilder( + val ctx: MutableMap + ) { + context fun put(k: K, v: V) = ctx.put(k, v) + } + ``` + +## Callable references + +**§19** *(callable references, eager resolution)*: References to callables declared with context parameters are resolved **eagerly**: + +* The required context parameters must be resolved in the context in which the reference is created, +* The resulting type does not mention context. + +```kotlin +class User +val user = User() + +context(users: UserService) fun User.doStuff(x: Int): Int = x + 1 + +// val x = User::doStuff // unresolved +// val y = user::doStuff // unresolved + +context(users: UserService) fun example() { + val g = User::doStuff // resolve context, g: User.() -> Int + // you need to explicitly write the lambda below + val h: context(UserService) User.() -> Int = { doStuff() } +} +``` + +**§20** *(callable references, motivation)*: This design was motivated by the pattern of having a set of functions sharing a common context parameter, like in the example below. + +```kotlin +context(users: UserService) fun save(u: User): Unit { ... } +context(users: UserService) fun saveAll(users: List): Unit = + users.forEach(::save) // ::save is resolved as (User) -> Unit +``` + +**§21** *(callable references, future)*: We consider as **future** improvement a more complex resolution of callables, in which the context is taken into account when the callable is used as an argument of a function that expects a function type with context. + +## Context and classes + +**§22** *(no contexts in constructors)*: We do **not** support contexts in constructor declarations (neither primary nor secondary). There are some issues around their design, especially when mixed with inheritance and private/protected visibility. + +**§23** *(no contexts in constructors, workaround)*: Note that Kotlin is very restrictive with constructors, as it doesn't allow them to be `suspend` either; the same workarounds (companion object + invoke, function with the name of the class) are available in this case. + +**§24** *(no contexts in constructors, future)*: We have defined four levels of how this support may pan out in the future: + +1. No context for constructors (current one), +2. Contexts only for secondary constructors, +3. Contexts also in primary constructors, but without the ability to write `val`/`var` in front of them, +4. Support for `val`/`var` for context parameter, but without entering the context, +5. Context parameters declared with `val`/`var` enter the context of every declaration. + +**§25** *(no contexts in class declarations)*: The prototype also contains a "context in class" feature, which both adds a context parameter to the constructor, and makes that value available implicitly in the body of the class. We've explored some possibilities, but the conclusion was that we do not know at this point which is the right one. Furthermore, we think that "scoped properties" may bring a better overall solution to this problem; and adding this feature now would get in the way. + +## Technical design + +### Syntax + +**§26** *(`context` is a modifier)*: Everybody's favorite topic! Although the current implementation places some restrictions on the location of the context block, the intention is to turn it (syntactically) into a modifier. In terms of the Kotlin grammar, + +``` +functionModifier: ... | context +propertyModifier: ... | context + +context: 'context' [ '(' [ contextParameter { ',' contextParameter } ] ')' ] +contextParameter: receiverType | parameter + +functionType: [ functionContext ] {NL} [ receiverType {NL} '.' {NL} ] ... +functionContext: 'context' [ '(' [ receiverType { ',' receiverType } ] ') ] +``` + +**Recommended style:** annotations, context parameters, other modifiers as per the [usual style guide](https://kotlinlang.org/docs/coding-conventions.html#modifiers-order). + +### Extended resolution algorithm + +**§27** *(scope with contexts)*: When **building the scope** corresponding to a callable we need to mark the new elements accordingly. + +* Context receivers are added to the scope and marked as such (so differently from the default receiver). They have lower priority than other receivers. +* Named context parameters are added in the same way as value parameters, and marked as such. They have the same priority as regular value parameters. + +Note that context receivers are **not** added as default receivers. In particular, this means they are **not** accessible through the `this` reference. + +**§28** *(resolution, candidate sets)*: When **building candidate sets** during resolution, we consider context receivers part of the implicit scope, in a similar way as default receivers. However: + +* We consider only member callables which are marked with `context`. +* When considering callables where the type of the context receiver appears as extension or dispatch receiver, we consider only those marked with `context`. + +Remember that the candidate set from context receivers has less priority than those from other receivers. + +```kotlin +class A { context fun foo(x: Int) } +class B { fun foo(x: Object) } + +// here 'foo' resolves to the one in 'B' +// because 'B' is introduced in an inner scope +// the fact that the one from 'A' is more specific doesn't play a role +context(A) B.example1() = foo(5) + +// 'this' always resolves to 'B' +context(A) B.example2() = this.foo(5) + +// to access the members from 'A' use a name, 'summon' +context(A) B.example3a() = summon().foo(5) +context(a: A) B.example3b() = a.foo(5) +context(a: A) B.example3b() = with(a) { foo(5) } +``` + +**§29** *(resolution, most specific candidate)*: When choosing the **most specific candidate** we follow the Kotlin specification, with one addition: + +* Candidates with declared context (even if empty) are considered more specific than those without a context. +* In particular, a declared but empty context is not more specific than one with 1 or more context parameters. + +**§30** *(resolution, context resolution)*: Once the overload candidate is chosen, we **resolve** context parameters (if any). For each context parameter: + +* We traverse the tower of scopes looking for **exactly one** default receiver, context receivers, or named context parameter with a compatible type. +* It is an ambiguity error if more than one value is found at the same level. +* It is an overload resolution error if no applicable context parameter is found. + +We stress that each context parameter is resolved separately. There is no special "link" between all the context receivers and arguments introduced in the same declaration. + +```kotlin +interface Logger { + context fun log(message: String) +} +class ConsoleLogger: Logger { ... } +class FileLogger: Logger { ... } + +context(Logger) fun logWithTime(message: String) = ... + +context(ConsoleLogger, FileLogger) fun example1a() = + logWithTime("hello") // ambiguity error + +context(console: ConsoleLogger, file: FileLogger) fun example1b() = + logWithTime("hello") // ambiguity error + +context(FileLogger) fun example2() = context(ConsoleLogger()) { + logWithTime("hello") // no ambiguity, uses the new ConsoleLogger +} + +context(console: ConsoleLogger, file: FileLogger) fun example3() = + with(console) { logWithTime("hello") } // no ambiguity, uses 'console' + +context(ConsoleLogger) fun FileLogger.example4() = + logWithTime("hello") // no ambiguity, uses FileLogger +``` + +### JVM ABI and Java compatibility + +**§31** *(JVM and Java compatibility)*: In the JVM a function with context parameters is represented as a regular function with the context parameters situated at the *beginning* of the parameter list. The name of the context parameter, if present, is used as the name of the parameter. + +* Note that parameter names do not impact JVM ABI compatibility. + +### Alternative scoping + +**§31** *(All receivers in the same scope)*: We have designed an alternative in which all parameters and receivers of a declaration form a single scope. However, we have decided against it, because we wanted the addition of a context in an already-existing function to introduce as few clashes as possible. + +```kotlin +class A { context fun foo(x: Int) } +class B { fun foo(x: Object) } + +// here 'foo' resolves to the one in 'A' +// because both 'A' and 'B' are in the same scope +// but the one from 'A' is more specific +context(A) B.example1() = foo(5) +``` + +## Q&A about design decisions + +*Q: Why introduce the concept of contextual visibility?* + +One of the main objections to the previous design was the potential scope pollution. Although named context parameters make this issue less pressing, there are still problems with bringing a whole context receiver into implicit scope. + +In particular, some functions like `toString`, `equals`, ... become ambiguous — or even worse, they resolve to something different from what the developer intended. Since they are declared without context, those functions won't be accessible from context receivers. + +*Q: Why only give the author of a callable the ability to add an empty context, and as a result make that callable available from a context receiver?* + +We've toyed with several designs here (annotation in the declaration, annotation in the import), but named context parameter + `with` seems to work just fine, and it doesn't bring any new piece to the mix. + +*Q: Why do callable references resolve differently with context parameters and extension receivers?* + +We've actually considered five different designs; there are the reason we've decided for the described one. + +* Even though extension (and dispatch) receivers are implicit in function calls, they are explicit in callable references — think of `User::save`. This is not the case for context parameters. +* We found the use case of passing a reference to a function such `map`, while reusing the context from the outer declaration, a quite important one. + +Note that we intend to improve our design to account for additional cases in which this rule fails, but taking the context parameters succeeds. However, this is part of a larger process of function conversions, in which we also want to consider similar behavior for suspend, composable, and other modifiers. + +*Q: Why drop the subtyping restriction between context receivers?* + +One important piece of feedback in the previous design was the need for functions that introduce more than one value in the context. In turn, that means providing a function such as the following. + +```kotlin +fun context(a: A, b: B, block: context(A, B) () -> R): R +``` + +The subtyping restriction would disallow this case, since `A` and `B` may (potentially) be subtypes of one another. We've considered adding some sort of additional restrictions at the type level or making this function a built-in, but in the end, we've decided that it's the developer's responsibility to keep a clean and ordered context. An ambiguity error seems reasonable if the problem really arises at the use site. + +*Q: Why drop the context-in-class feature altogether?* + +As we explored the design, we concluded that the interplay between contexts and inheritance and visibility is quite complex. Think of questions such as whether a context receiver to a class should also be in implicit scope in an extension method to that class, and whether that should depend on the potential visibility of such context receiver. At this point, we think that a good answer would only come if we fully design "`with`` properties", that is, values which also enter the context scope in their block. + +An additional stone in the way is that one commonly requested use case is to have a constructor with value parameters, and other where some of those are contextual. However, this leads to platform clashes, so we would need an additional design on that part. + +## Acknowledgements + +We thank everybody who has contributed to improving and growing the context receiver proposal to this current form. Several people have devoted a significant amount of time being interviewed and sharing their thoughts with us, showing the involvement of the Kotlin community in this process. From 02eda347b075fd40f378bd13b184f0bc6accfa29 Mon Sep 17 00:00:00 2001 From: Alejandro Serrano Date: Thu, 21 Dec 2023 16:47:07 +0100 Subject: [PATCH 02/28] Link to discussion --- proposals/context-parameters.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/proposals/context-parameters.md b/proposals/context-parameters.md index 8e2bf59eb..22d40c2e9 100644 --- a/proposals/context-parameters.md +++ b/proposals/context-parameters.md @@ -3,7 +3,7 @@ * **Type**: Design proposal * **Author**: Alejandro Serrano * **Contributors**: Marat Ahkin, Nikita Bobko, Ilya Gorbunov, Mikhail Zarechenskii, Denis Zharkov -* **Discussion**: [KEEP-?](https://github.com/Kotlin/KEEP/issues/?) +* **Discussion**: [KEEP-367](https://github.com/Kotlin/KEEP/issues/367) ## Abstract From 1d27d930cd9cf38cabdd363d47f8a6d815afbd54 Mon Sep 17 00:00:00 2001 From: Alejandro Serrano Date: Thu, 21 Dec 2023 20:23:12 +0100 Subject: [PATCH 03/28] Update proposals/context-parameters.md Co-authored-by: Luca Kellermann --- proposals/context-parameters.md | 1 - 1 file changed, 1 deletion(-) diff --git a/proposals/context-parameters.md b/proposals/context-parameters.md index 22d40c2e9..70cd89f6d 100644 --- a/proposals/context-parameters.md +++ b/proposals/context-parameters.md @@ -326,7 +326,6 @@ fun ResourceScope.openFile(file: File): InputStream context fun ResourceScope.openFile(file: File): InputStream // The API is nicer using context receivers -@JvmName("contextualFileOpen") // needed to avoid platform clash context(ResourceScope) fun File.open(): InputStream ``` From 1c7a84788e043422d06fab2473aa8c20d9901642 Mon Sep 17 00:00:00 2001 From: Alejandro Serrano Date: Fri, 22 Dec 2023 10:08:47 +0100 Subject: [PATCH 04/28] Missing `fun` --- proposals/context-parameters.md | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/proposals/context-parameters.md b/proposals/context-parameters.md index 70cd89f6d..5bdf1a28b 100644 --- a/proposals/context-parameters.md +++ b/proposals/context-parameters.md @@ -447,15 +447,15 @@ class B { fun foo(x: Object) } // here 'foo' resolves to the one in 'B' // because 'B' is introduced in an inner scope // the fact that the one from 'A' is more specific doesn't play a role -context(A) B.example1() = foo(5) +context(A) fun B.example1() = foo(5) // 'this' always resolves to 'B' -context(A) B.example2() = this.foo(5) +context(A) fun B.example2() = this.foo(5) // to access the members from 'A' use a name, 'summon' -context(A) B.example3a() = summon().foo(5) -context(a: A) B.example3b() = a.foo(5) -context(a: A) B.example3b() = with(a) { foo(5) } +context(A) fun B.example3a() = summon().foo(5) +context(a: A) fun B.example3b() = a.foo(5) +context(a: A) fun B.example3b() = with(a) { foo(5) } ``` **§29** *(resolution, most specific candidate)*: When choosing the **most specific candidate** we follow the Kotlin specification, with one addition: @@ -514,7 +514,7 @@ class B { fun foo(x: Object) } // here 'foo' resolves to the one in 'A' // because both 'A' and 'B' are in the same scope // but the one from 'A' is more specific -context(A) B.example1() = foo(5) +context(A) fun B.example1() = foo(5) ``` ## Q&A about design decisions From 52e805c214fb412e1a3fec68ecd7271a570ab7ab Mon Sep 17 00:00:00 2001 From: Alejandro Serrano Date: Sat, 23 Dec 2023 08:56:42 +0100 Subject: [PATCH 05/28] Update proposals/context-parameters.md Co-authored-by: Gleb Minaev <43728100+lounres@users.noreply.github.com> --- proposals/context-parameters.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/proposals/context-parameters.md b/proposals/context-parameters.md index 5bdf1a28b..1450c04b9 100644 --- a/proposals/context-parameters.md +++ b/proposals/context-parameters.md @@ -2,7 +2,7 @@ * **Type**: Design proposal * **Author**: Alejandro Serrano -* **Contributors**: Marat Ahkin, Nikita Bobko, Ilya Gorbunov, Mikhail Zarechenskii, Denis Zharkov +* **Contributors**: Marat Akhin, Nikita Bobko, Ilya Gorbunov, Mikhail Zarechenskii, Denis Zharkov * **Discussion**: [KEEP-367](https://github.com/Kotlin/KEEP/issues/367) ## Abstract From 42772efdd585c2ebd7d582fbd6a5a398c15cf0b9 Mon Sep 17 00:00:00 2001 From: Alejandro Serrano Date: Thu, 18 Jan 2024 12:32:32 +0100 Subject: [PATCH 06/28] New iteration --- proposals/context-parameters.md | 495 +++++++++++++------------------- 1 file changed, 196 insertions(+), 299 deletions(-) diff --git a/proposals/context-parameters.md b/proposals/context-parameters.md index 5bdf1a28b..6f76f382e 100644 --- a/proposals/context-parameters.md +++ b/proposals/context-parameters.md @@ -11,354 +11,303 @@ This is an updated proposal for [KEEP-259](https://github.com/Kotlin/KEEP/issues This document is not (yet) formally a KEEP, since it lacks some of the technical elements. Those are going to be provided at a later time, but we thought it would be interesting to open the discussion even if the design is not fully formalized. -### Summary of changes from [previous proposal](https://github.com/Kotlin/KEEP/issues/259) +### Summary of changes from the [previous proposal](https://github.com/Kotlin/KEEP/issues/259) 1. Introduction of named context parameters, -2. Removal of `this@Type` syntax, introduction of `summon()`, -3. Not every member of a context receiver is accessible, only those with a context, -4. No subtyping check between different context parameters, -5. Contexts are not allowed in constructors, -6. Callable references resolve their context arguments eagerly, -7. Context-in-classes are dropped. +2. Context receivers are dropped, +3. Removal of `this@Type` syntax, introduction of `context()`, +4. Contexts are not allowed in constructors, +5. Callable references resolve their context arguments eagerly, +6. Context-in-classes are dropped. ## Table of contents * [Abstract](#abstract) - * [Summary of changes from previous proposal](#summary-of-changes-from-previous-proposal) + * [Summary of changes from the previous proposal](#summary-of-changes-from-the-previous-proposal) * [Table of contents](#table-of-contents) * [Members with context parameters](#members-with-context-parameters) +* [Standard library support](#standard-library-support) + * [Reflection](#reflection) +* [Simulating receivers](#simulating-receivers) * [Use cases](#use-cases) * [As implicits](#as-implicits) * [As scopes](#as-scopes) * [For extending DSLs](#for-extending-dsls) * [Context-oriented dispatch / externally-implemented interface / type classes](#context-oriented-dispatch--externally-implemented-interface--type-classes) * [Dependency injection](#dependency-injection) -* [Standard library support](#standard-library-support) - * [Reflection](#reflection) -* [Context receivers](#context-receivers) - * [⚠️ Preliminary warning](#️-preliminary-warning) - * [Context receivers and scope](#context-receivers-and-scope) - * [The migration story for receivers](#the-migration-story-for-receivers) * [Callable references](#callable-references) * [Context and classes](#context-and-classes) * [Technical design](#technical-design) * [Syntax](#syntax) * [Extended resolution algorithm](#extended-resolution-algorithm) * [JVM ABI and Java compatibility](#jvm-abi-and-java-compatibility) - * [Alternative scoping](#alternative-scoping) * [Q\&A about design decisions](#qa-about-design-decisions) -* [Acknowledgements](#acknowledgements) +* [Acknowledgments](#acknowledgments) ## Members with context parameters -**§1** *(context parameters)*: Every callable member (functions — but not constructors — and properties) gets additional support for **context parameters**. Context parameters are declared with the `context` keyword followed by a list in parentheses. There are two kinds of context parameters: +**§1** *(declaration)*: Every callable member (functions — but not constructors — and properties) gets additional support for **context parameters**. Context parameters are declared with the `context` keyword followed by a list of parameters, each of the form `name: Type`. -* **Named context parameters:** with a name, using `name: Type` syntax. -* **Context receivers:** without a name, simply listing the type. +* Within the body of the declared member, the value of the context parameter is accessible using its name, similar to value parameters. +* It is allowed to use `_` as a name; in that case, the value is not accessible through any name (but still participates in context resolution). -These mirror the two kinds of parameters we can currently declare in Kotlin: value parameters, and receivers. The difference is that they are **implicitly** passed using other context arguments. - -```kotlin -context(logger: Logger) fun User.doAction() { ... } -``` +**§2** *(restrictions)*: -**§2** *(context parameters, restrictions)*: +* It is an *error* to declare an **empty** list of context parameters. +* It is an *error* if the **name** of a context parameter **coincides** with the name of another context or value parameter to the callable (except for multiple uses of `_`). -* It is an *error* if the **name** of a context parameter **coincides** with the name of another context or value parameter to the callable. -* We want to warn the user (warning, inspection) if there are two context receivers for which one is a **supertype** of (or equals to) the other, since in that case the shared members would always result in an ambiguity error. - * This was an error in the previous iteration of the design. +**§3** *(implicitness)*: When calling a member with context parameters, those are not spelled out. Rather, the value for each of those arguments is **resolved** from two sources: in-scope context parameters, and implicit scope. We say that context parameters are **implicit**. -**§3** *(context parameters, order)*: Named context parameters and context receivers may be freely interleaved in a `context` declaration. - -* Although people have already referred to some style guidelines, we prefer those to arise organically from the community. - -**§4** *(empty context)*: The context may be empty, in which case we can write either `context()` or drop the parentheses to get `context`. +```kotlin +context(logger: Logger) fun logWithTime(message: String) = + logger.log("${LocalDateTime.now()}: $message") -* This affects contextual visibility, as explained below. -* It is useless to declare an empty context in a function without an extension or dispatch receiver, so that case may warrant a warning. -* It is implementation-dependent whether it is allowed to declare two callables with the same signature, except for an empty context in one of them. - * In the case of Kotlin/JVM, this results in a platform clash unless one of them is renamed. +context(logger: Logger) fun User.doAction() { + logWithTime("saving user $id") + // ... +} +``` -**§5** *(override)*: Context parameters are part of the signature, and follow the same rules concerning overriding: +**§4** *(override)*: Context parameters are part of the signature, and follow the same rules as regular value parameters concerning overriding: * When overriding, the type and order of context parameters must coincide. - * Even if the context is empty, you cannot override a function without context with one with it, or vice versa. -* It is allowed (yet discouraged) to change the name of a context parameter or its receiver/named status. +* It is allowed (yet discouraged) to change the name of a context parameter. -**§6** *(naming ambiguity)*: We use the term **context** with two meanings: +**§5** *(naming ambiguity)*: We use the term **context** with two meanings: 1. For a declaration, it refers to the collection of context parameters declared in its signature. We also use the term *contextual function or property*. -2. Within a body, we use context to refer to the combination of the implicit scope, context receivers, and context parameters. This context is the source for context resolution, that is, for "filling in" the implicit context parameters. +2. Within a body, we use context to refer to the combination of the implicit scope and context parameters. This context is the source for context resolution, that is, for "filling in" the implicit context parameters. -**§7** *(function types)*: **Function types** are extended with context parameters, following the same syntax as we have for declarations. However, we do **not** support named context parameters in function types. +**§6** *(function types)*: **Function types** are extended with context parameters. It is only allowed to mention the *type* of context parameters, names are not supported. -* That way we don't have to inspect the body of a lambda because of this. The reasoning is similar to how we restrict lambdas with no declared arguments to be 0 or 1-ary. +* We do not want to inspect the body of a lambda looking for different context parameter names during overload resolution. The reasoning is similar to how we restrict lambdas with no declared arguments to be 0 or 1-ary. ```kotlin context(Transaction) (UserId) -> User? context(Logger) User.() -> Int ``` -Note that, like in the case of regular receivers, those types are considered equivalent (for typing purposes, **not** for resolution purposes) to the function types in which all parameters are declared as "regular" ones. +Note that, like in the case of extension receivers, those types are considered equivalent (for typing purposes, **not** for resolution purposes) to the function types in which all parameters are declared as value parameters. -## Use cases +**§7** *(lambdas)*: If a lambda is assigned a function type with context parameters, those behave as if declared with `_` as its name. -This is a recollection and categorization of the different use cases we have found for context parameters, including guidelines on which kind of parameter is more applicable (if so). - -### As implicits - -**§A** *(implicit use case)*: In this case, the context parameter is thought of as a set of services available to a piece of code, but without the ceremony of passing those services explicitly in every call. - -* In most cases, those contexts are introduced with a name. -* The context parameter type may or may not have been designed to appear as context. - * For example, it comes from a Java library. - -A `Repository` class for a particular entity or a `Logger` are good examples of this mode of use. +* They participate in context resolution but are only accessible through the `context` function (defined below). ```kotlin -context(users: UserRepository) fun User.getFriends() = ... -``` +fun withConsoleLogger(block: context(Logger) () -> A) = ... -### As scopes +withConsoleLogger { + val logger = context() + // you can call functions with Logger as context parameter + logWithTime("doing something") +} +``` -**§B** *(scope use case)*: In this case we use the context parameter as a marker of being inside a particular scope, which unlocks additional abilities. A prime example is `CoroutineScope`, which adds `launch`, `async`, and so on. In this mode of use: +## Standard library support -* The `Scope` type is carefully designed and the functions marked as `context`, -* When used in a function, it appears as context receiver. +**§7** *(`context` function)*: To extend the implicit scope in a contextual manner we provide additional functions in the standard library. -This covers other types of scopes as `Transaction`, or `Resource`-related. The `Raise` and `ResourceScope` DSLs from the Arrow project also fit this view. +* The implementation may be built into the compiler, instead of having a plethora of functions defined in the standard library. ```kotlin -// currently uses extension receivers -fun ResourceScope.openFile(file: File): InputStream -// but the API is nicer using context receivers -context(ResourceScope) fun File.open(): InputStream +fun context(context: A, block: context(A) () -> R): R = block(context) +fun context(a: A, b: B, block: context(A, B) () -> R): R = block(a, b) +fun context(a: A, b: B, c: C, block: context(A, B, C) () -> R): R = block(a, b, c) ``` -We describe later in this document how library authors should ponder context parameters into their existing design. - -### For extending DSLs - -**§C** *(DSLs use case)*: In this case, contexts are used to provide new members available in a domain-specific language. Currently, this is approached by declaring an interface which represents the "DSL context", and then have member or extension functions on that interface. +**§8** *(`context` function)*: We also provide a generic way to obtain a value by type from the context. It allows access to context parameters even when declared using `_`, or within the body of a lambda. ```kotlin -interface HtmlScope { - fun body(block: HtmlScope.() -> Unit) -} +context(ctx: A) fun context(): A = ctx ``` -Context parameters lift two of the main restrictions of this mode of use: - -* It's possible to add new members with extension receiver without modifying the Scope class itself. -* It's possible to add members which are only available when the DSL Scope has certain type arguments. +This function replaces the uses of `this@Type` in the previous iteration of the design. -### Context-oriented dispatch / externally-implemented interface / type classes +### Reflection -**§D** *(Context-oriented dispatch use case)*: Context receivers can be used to simulate functions available for a type, by requiring an interface that uses the type as an argument. This is very similar to type classes in other languages. +**§10** *(callable reflection)*: The following additions to the `kotlin.reflect` are required for information about members. ```kotlin -interface Comparator { - context fun T.compareTo(other: T): Boolean +interface KParameter { + enum class Kind { + INSTANCE, EXTENSION_RECEIVER, VALUE, + CONTEXT_PARAMETER // new + } } -context(Comparator) fun max(x: T, y: T) = - if (x.compareTo(y) > 0) x else y +val KCallable<*>.contextParameters: List + get() = parameters.filter { + it.kind == KParameter.Kind.CONTEXT_RECEIVER + } + ``` -### Dependency injection +**§11** *(property reflection)*: Properties with context parameters are not `KProperty0`, `1`, nor `2`, but rather simply `KProperty`. -**§E.1** *(Dependency injection use case)*: You can view context parameters as values that must be "injected" from the context for the code to work. Since context arguments are completely resolved at compile-time, this provides something like dependency injection in the language. +## Simulating receivers -**§E.1** *(Dependency injection use case, companion object)*: In some cases, you may want to define that instantiating a certain class requires some value — this creates the typical hierarchy of dependency we see in DI frameworks. We can accomplish this by faking a constructor with those contexts: +There are cases where the need to refer to members of a context parameter through its name may hurt readability — this happens mostly in relation to DSLs. In this section, we provide guidance on how to solve this issue. -```kotlin -interface Logger { ... } -interface UserService { ... } +**§12** *(bridge function)*: Given an interface with some members, -class DbUserService(val logger: Logger, val connection: DbConnection) { - companion object { - context(logger: Logger, connection: Connection) - operator fun invoke(): DbUserService = DbUserService(logger, connection) - } +```kotlin +interface Raise { + fun raise(error: Error): Nothing } ``` -**§E.2** *(Dependency injection use case, discouragement)*: Note that we suggest against this feature, since having parameters explicitly reduces the need to nest too many `context` calls. +we want to let users call `raise` whenever `Raise` is in scope, without having to mention the context parameter, but ensuring that the corresponding value is part of the context. ```kotlin -// do not do this -context(ConsoleLogger(), ConnectionPool(2)) { - context(DbUserService()) { - ... +context(_: Raise) fun Either.bind(): A = + when (this) { + is Left -> raise(error) // instead of r.raise + is Right -> value } -} - -// better be explicit about object creation -val logger = ConsoleLogger() -val pool = ConnectionPool(2) -val userService = DbUserService(logger, pool) -// and then inject everything you need in one go -context(logger, userService) { - ... -} ``` -## Standard library support - -**§8** *(`context` function)*: To extend the implicit scope in a contextual manner we provide additional functions in the standard library. - -* The implementation may be built into the compiler, instead of having a plethora of functions defined in the standard library. +We do this by introducing a **bridge function** that simply wraps the access to the context parameter. ```kotlin -fun context(context: A, block: context(A) () -> R): R = block(context) -fun context(a: A, b: B, block: context(A, B) () -> R): R = block(a, b) -fun context(a: A, b: B, c: C, block: context(A, B, C) () -> R): R = block(a, b, c) +context(r: Raise) inline fun raise(error: Error): Nothing = r.raise(error) ``` -**§9** *(`summon` function)*: We also provide a generic way to obtain a value by type from the context, - -```kotlin -context(ctx: A) fun summon(): A = ctx -``` +**§12** *(receiver migration, members)*: If your library exposes a "scope" or "context" type, we suggest moving to context parameters: -This function replaces the uses of `this@Type` in the previous iteration of the design. +1. functions with the scope type as extension receiver should be refactored to use context parameters, +2. operations defined as members and extending other types should be taken out of the interface definition, if possible, +3. operations defined as members of the scope type should be exposed additionally with bridge functions. -### Reflection +*How do you know that your type `S` is of such kind?* -**§10** *(callable reflection)*: The following additions to the `kotlin.reflect` are required for information about members. +* It's often used as extension receiver, but without being the "subject" of the action. +* There's some "run" function that takes a `S.() → R` function as an argument. ```kotlin -interface KParameter { - enum class Kind { - INSTANCE, EXTENSION_RECEIVER, VALUE, - **NAMED_CONTEXT****,** **CONTEXT_RECEIVER** - } +fun Raise.foo() = ... +// should become +context(_: Raise) fun foo() = ... + +interface Raise { + fun Either.bind() = ... } +// this function can be exposed now as follows +context(_: Raise) fun Either.bind() = ... +``` -val KCallable<*>.declaresContext: Boolean +**§13** *(receiver migration, run functions)*: We advise keeping any function taking a lambda where the scope type appears as extension receiver as is, and provide a new variant with a context parameter. The latter must unfortunately get a different name to prevent overload conflicts. -val KCallable<*>.contextParameters: List - get() = parameters.filter { - it.kind == KParameter.Kind.NAMED_CONTEXT || it.kind == KParameter.Kind.CONTEXT_RECEIVER - } - -// +```kotlin +fun runRaise(block: Raise.() -> A): Either +// provide the additional variant +fun runRaiseContext(block: context(Raise) () -> A): Either ``` -It is possible that `(c.declaredContext && c.contextParameters.isEmpty())` is true if the callable declares an empty context +**§14** *(receiver migration, source compatibility)*: The rules above guarantee source compatibility for users of the interface. -**§11** *(property reflection)*: Properties with context receivers are not `KProperty0`, `1`, nor `2`, but rather simply `KProperty`. +## Use cases -## Context receivers +This is a recollection and categorization of the different use cases we have found for context parameters, including guidelines on which kind of parameter is more applicable (if so). -### ⚠️ Preliminary warning +### As implicits -**§12** *(context receivers, warning)*: Although the previous iteration of this design was called *context receivers*, and this document spends most of the time speaking about them because they pose a greater design challenge, those are *not* the main use mode of context parameters. +**§A** *(implicit use case)*: In this case, the context parameter is thought of as a set of services available to a piece of code, but without the ceremony of passing those services explicitly in every call. In most cases, those contexts are introduced with an explicit name. -From a documentation (and educational) perspective, we emphasize that *named context parameters* solve many of the problems related to implicitly passing information around, without the caveat of scope pollution. +A `Repository` class for a particular entity or a `Logger` are good examples of this mode of use. ```kotlin -context(scope: CoroutineScope) fun User.doSomething() = scope.launch { ... } +context(users: UserRepository) fun User.getFriends() = ... ``` -Context receivers have a narrower use case of delimiting a *context* and making some additional functions only available *within* them. For technical reasons, context receivers (and not named context parameters) also appear in function types, but those types are not usually written by the developers themselves. +### As scopes -### Context receivers and scope +**§B** *(scope use case)*: In this case we use the context parameter as a marker of being inside a particular scope, which unlocks additional abilities. A prime example is `CoroutineScope`, which adds `launch`, `async`, and so on. In this mode of use: -**§13** *(context receivers, motivation)*: we want context receivers to expose a carefully designed API, so the design takes this particular mode of usage into account. For that reason, we ask library designers to think about which members are available when their types are used as context receivers. +* The `Scope` type is carefully designed, and the functions are exposed using bridge functions, +* In most cases, the context parameter has no name or is irrelevant. -**§14** *(contextual visibility)*: Concretely, whenever a context receiver is brought into scope, only the members with a declared context are available without qualification. Note that this includes those with an 0-ary context, which can be thought of as a "marker" for contextual visibility. +This covers other types of scopes as `Transaction`, or `Resource`-related. The `Raise` and `ResourceScope` DSLs from the Arrow project also fit this view. ```kotlin -interface Logger { - context fun log(message: String) // context() fun log(..) - fun thing() -} - -fun Logger.shout(message: String) = log(message.uppercase()) - -context fun Logger.whisper(message: String) = log(message.lowercase()) +// currently uses extension receivers +fun ResourceScope.openFile(file: File): InputStream +// but the API is nicer using context parameters +context(_: ResourceScope) fun File.open(): InputStream +``` -context(Logger) fun logWithName(name: String, message: String) = - log("$name: $message") +### For extending DSLs -context(Logger) fun doSomething() { - log("Hello") // ok - thing() // unresolved reference 'thing' - summon().thing() // ok - shout("Hello") // unresolved reference 'shout' - whisper("Hello") // ok - logWithName("Alex", "Hello") // ok -} +**§C** *(DSLs use case)*: In this case, contexts are used to provide new members available in a domain-specific language. Currently, this is approached by declaring an interface that represents the "DSL context", and then having member or extension functions on that interface. -fun somethingElse() { - log("Hello") // unresolved reference 'log' - logWithName("Alex", "Hello") // no context receiver for 'Logger' found +```kotlin +interface HtmlScope { + fun body(block: HtmlScope.() -> Unit) } ``` -### The migration story for receivers - -**§15** *(receiver migration, caveats)*: It is **not** desirable to change existing APIs from extension receivers to context parameters, because of the potential changes in resolution (different meaning of `this`, changes in callable references). This is especially relevant for libraries which are consumed by virtually everybody (standard library, coroutines). - -**§16** *(receiver migration, scope types)*: If your library exposes a "scope" or "context" type, we suggest adding an empty context to their members as soon as possible. Every extension function over this type also needs that modifier. - -*How do you know that your type `S` is of such kind?* +Context parameters lift two of the main restrictions of this mode of use: -* It's often used as extension receiver, but without being the "subject" of the action. -* There's some "run" function that takes a `S.() → R` function as an argument. +* It's possible to add new members with an extension receiver without modifying the Scope class itself. +* It's possible to add members which are only available when the DSL Scope has certain type arguments. +### Context-oriented dispatch / externally-implemented interface / type classes -The benefit for the consumer of the library is that now they can write functions where the type is a context receiver. This can lead to nicer-to-read code, since it "frees the extension position". +**§D** *(context-oriented dispatch use case)*: Context parameters can be used to simulate functions available for a type, by requiring an interface that uses the type as an argument. This is very similar to type classes in other languages. ```kotlin -context(ApplicationContext) fun User.buildHtml() = ... -``` +interface Comparator { + fun compareTo(one: T, other: T): Boolean +} -**§17** *(receiver migration, functions with subject)*: We encourage library authors to review their API, look for those cases, and provide new variants based on context parameters, instead of merely marking the original function with context. +context(comparator: Comparator) fun T.compareTo(other: T): Boolean = + comparator.compareTo(one, other) -```kotlin -// Uses extension receivers, but File is in fact the "subject" -fun ResourceScope.openFile(file: File): InputStream +context(_: Comparator) fun max(x: T, y: T) = + if (x.compareTo(y) > 0) x else y +``` -// Doing this is OK, but provides no additonal benefit -context fun ResourceScope.openFile(file: File): InputStream +### Dependency injection -// The API is nicer using context receivers -context(ResourceScope) fun File.open(): InputStream -``` +**§E.1** *(dependency injection use case)*: You can view context parameters as values that must be "injected" from the context for the code to work. Since context arguments are completely resolved at compile-time, this provides something like dependency injection in the language. -**§18** *(receiver migration, alternatives)*: Our design strongly emphasizes that library authors are key in exposing their API using context receivers. As we discussed above, named context parameters should be first considered. However, if you really want this ability for types you do not control, there are two possibilities. +**§E.1** *(dependency injection use case, companion object)*: In some cases, you may want to define that instantiating a certain class requires some value — this creates the typical hierarchy of dependency we see in DI frameworks. We can accomplish this by faking a constructor with those contexts: -* Named context + `with` +```kotlin +interface Logger { ... } +interface UserService { ... } - ```kotlin - context(ctx: ApplicationContext) fun User.buildHtml() = with(ctx) { ... } - ``` +class DbUserService(val logger: Logger, val connection: DbConnection) { + companion object { + context(logger: Logger, connection: Connection) + operator fun invoke(): DbUserService = DbUserService(logger, connection) + } +} +``` -* Lightweight wrapper type that exposes the functionality as contextual +**§E.2** *(dependency injection use case, discouragement)*: Note that we suggest against this feature, since having parameters explicitly reduces the need to nest too many `context` calls. - ```kotlin - // if it's an interface, you can delegate - @JvmInline value class ApplicationContext( - val app: Application - ) { - context fun get() = app.get() - // copy of every function, but with context on front - } +```kotlin +// do not do this +context(ConsoleLogger(), ConnectionPool(2)) { + context(DbUserService()) { + ... + } +} - // you can also use it to limit the available methods - @JvmInline value class MapBuilder( - val ctx: MutableMap - ) { - context fun put(k: K, v: V) = ctx.put(k, v) - } - ``` +// better be explicit about object creation +val logger = ConsoleLogger() +val pool = ConnectionPool(2) +val userService = DbUserService(logger, pool) +// and then inject everything you need in one go +context(logger, userService) { + ... +} +``` ## Callable references -**§19** *(callable references, eager resolution)*: References to callables declared with context parameters are resolved **eagerly**: +**§15** *(callable references, eager resolution)*: References to callables declared with context parameters are resolved **eagerly**: * The required context parameters must be resolved in the context in which the reference is created, * The resulting type does not mention context. @@ -379,7 +328,7 @@ context(users: UserService) fun example() { } ``` -**§20** *(callable references, motivation)*: This design was motivated by the pattern of having a set of functions sharing a common context parameter, like in the example below. +**§16** *(callable references, motivation)*: This design was motivated by the pattern of having a set of functions sharing a common context parameter, like in the example below. ```kotlin context(users: UserService) fun save(u: User): Unit { ... } @@ -387,15 +336,15 @@ context(users: UserService) fun saveAll(users: List): Unit = users.forEach(::save) // ::save is resolved as (User) -> Unit ``` -**§21** *(callable references, future)*: We consider as **future** improvement a more complex resolution of callables, in which the context is taken into account when the callable is used as an argument of a function that expects a function type with context. +**§17** *(callable references, future)*: We consider as **future** improvement a more complex resolution of callables, in which the context is taken into account when the callable is used as an argument of a function that expects a function type with context. ## Context and classes -**§22** *(no contexts in constructors)*: We do **not** support contexts in constructor declarations (neither primary nor secondary). There are some issues around their design, especially when mixed with inheritance and private/protected visibility. +**§18** *(no contexts in constructors)*: We do **not** support context parameters in constructor declarations (neither primary nor secondary). There are some issues around their design, especially when mixed with inheritance and private/protected visibility. -**§23** *(no contexts in constructors, workaround)*: Note that Kotlin is very restrictive with constructors, as it doesn't allow them to be `suspend` either; the same workarounds (companion object + invoke, function with the name of the class) are available in this case. +**§19** *(no contexts in constructors, workaround)*: Note that Kotlin is very restrictive with constructors, as it doesn't allow them to be `suspend` either; the same workarounds (companion object + `invoke``, function with the name of the class) are available in this case. -**§24** *(no contexts in constructors, future)*: We have defined four levels of how this support may pan out in the future: +**§20** *(no contexts in constructors, future)*: We have defined four levels of how this support may pan out in the future: 1. No context for constructors (current one), 2. Contexts only for secondary constructors, @@ -403,20 +352,19 @@ context(users: UserService) fun saveAll(users: List): Unit = 4. Support for `val`/`var` for context parameter, but without entering the context, 5. Context parameters declared with `val`/`var` enter the context of every declaration. -**§25** *(no contexts in class declarations)*: The prototype also contains a "context in class" feature, which both adds a context parameter to the constructor, and makes that value available implicitly in the body of the class. We've explored some possibilities, but the conclusion was that we do not know at this point which is the right one. Furthermore, we think that "scoped properties" may bring a better overall solution to this problem; and adding this feature now would get in the way. +**§21** *(no contexts in class declarations)*: The prototype also contains a "context in class" feature, which both adds a context parameter to the constructor and makes that value available implicitly in the body of the class. We've explored some possibilities, but the conclusion was that we do not know at this point which is the right one. Furthermore, we think that "scoped properties" may bring a better overall solution to this problem; and adding this feature now would get in the way. ## Technical design ### Syntax -**§26** *(`context` is a modifier)*: Everybody's favorite topic! Although the current implementation places some restrictions on the location of the context block, the intention is to turn it (syntactically) into a modifier. In terms of the Kotlin grammar, +**§22** *(`context` is a modifier)*: Everybody's favorite topic! Although the current implementation places some restrictions on the location of the context block, the intention is to turn it (syntactically) into a modifier. In terms of the Kotlin grammar, ``` functionModifier: ... | context propertyModifier: ... | context -context: 'context' [ '(' [ contextParameter { ',' contextParameter } ] ')' ] -contextParameter: receiverType | parameter +context: 'context' [ '(' [ parameter { ',' parameter } ] ')' ] functionType: [ functionContext ] {NL} [ receiverType {NL} '.' {NL} ] ... functionContext: 'context' [ '(' [ receiverType { ',' receiverType } ] ') ] @@ -426,112 +374,61 @@ functionContext: 'context' [ '(' [ receiverType { ',' receiverType } ] ') ] ### Extended resolution algorithm -**§27** *(scope with contexts)*: When **building the scope** corresponding to a callable we need to mark the new elements accordingly. - -* Context receivers are added to the scope and marked as such (so differently from the default receiver). They have lower priority than other receivers. -* Named context parameters are added in the same way as value parameters, and marked as such. They have the same priority as regular value parameters. - -Note that context receivers are **not** added as default receivers. In particular, this means they are **not** accessible through the `this` reference. - -**§28** *(resolution, candidate sets)*: When **building candidate sets** during resolution, we consider context receivers part of the implicit scope, in a similar way as default receivers. However: - -* We consider only member callables which are marked with `context`. -* When considering callables where the type of the context receiver appears as extension or dispatch receiver, we consider only those marked with `context`. - -Remember that the candidate set from context receivers has less priority than those from other receivers. - -```kotlin -class A { context fun foo(x: Int) } -class B { fun foo(x: Object) } - -// here 'foo' resolves to the one in 'B' -// because 'B' is introduced in an inner scope -// the fact that the one from 'A' is more specific doesn't play a role -context(A) fun B.example1() = foo(5) - -// 'this' always resolves to 'B' -context(A) fun B.example2() = this.foo(5) +**§23** *(declaration with context parameters)*: The context parameters declared for a callable are available in the same way as "regular" value parameters in the body of the function. Both value and context parameters are introduced in the same scope, there is no shadowing between them. -// to access the members from 'A' use a name, 'summon' -context(A) fun B.example3a() = summon().foo(5) -context(a: A) fun B.example3b() = a.foo(5) -context(a: A) fun B.example3b() = with(a) { foo(5) } -``` - -**§29** *(resolution, most specific candidate)*: When choosing the **most specific candidate** we follow the Kotlin specification, with one addition: +**§24** *(most specific candidate)*: When choosing the **most specific candidate** we follow the Kotlin specification, with one addition: -* Candidates with declared context (even if empty) are considered more specific than those without a context. -* In particular, a declared but empty context is not more specific than one with 1 or more context parameters. +* Candidates with context parameters are considered more specific than those without them. +* But there is no other prioritization coming from the length of the context parameter list or their types. -**§30** *(resolution, context resolution)*: Once the overload candidate is chosen, we **resolve** context parameters (if any). For each context parameter: +**§25** *(context resolution)*: Once the overload candidate is chosen, we **resolve** context parameters (if any). For each context parameter: -* We traverse the tower of scopes looking for **exactly one** default receiver, context receivers, or named context parameter with a compatible type. +* We traverse the tower of scopes looking for **exactly one** default receiver or context parameter with a compatible type. + * Anonymous context parameters (declared with `_`) also participate in this process. * It is an ambiguity error if more than one value is found at the same level. -* It is an overload resolution error if no applicable context parameter is found. - -We stress that each context parameter is resolved separately. There is no special "link" between all the context receivers and arguments introduced in the same declaration. +* It is an overload resolution error if no applicable value is found. ```kotlin interface Logger { - context fun log(message: String) + fun log(message: String) } class ConsoleLogger: Logger { ... } class FileLogger: Logger { ... } -context(Logger) fun logWithTime(message: String) = ... +context(logger: Logger) fun logWithTime(message: String) = ... -context(ConsoleLogger, FileLogger) fun example1a() = +context(console: ConsoleLogger, file: FileLogger) fun example1() = logWithTime("hello") // ambiguity error -context(console: ConsoleLogger, file: FileLogger) fun example1b() = - logWithTime("hello") // ambiguity error - -context(FileLogger) fun example2() = context(ConsoleLogger()) { +context(file: FileLogger) fun example2() = context(ConsoleLogger()) { logWithTime("hello") // no ambiguity, uses the new ConsoleLogger } context(console: ConsoleLogger, file: FileLogger) fun example3() = with(console) { logWithTime("hello") } // no ambiguity, uses 'console' - -context(ConsoleLogger) fun FileLogger.example4() = - logWithTime("hello") // no ambiguity, uses FileLogger ``` ### JVM ABI and Java compatibility -**§31** *(JVM and Java compatibility)*: In the JVM a function with context parameters is represented as a regular function with the context parameters situated at the *beginning* of the parameter list. The name of the context parameter, if present, is used as the name of the parameter. +**§26** *(JVM and Java compatibility)*: In the JVM a function with context parameters is represented as a regular function with the context parameters situated at the *beginning* of the parameter list. The name of the context parameter, if present, is used as the name of the parameter. * Note that parameter names do not impact JVM ABI compatibility. -### Alternative scoping - -**§31** *(All receivers in the same scope)*: We have designed an alternative in which all parameters and receivers of a declaration form a single scope. However, we have decided against it, because we wanted the addition of a context in an already-existing function to introduce as few clashes as possible. - -```kotlin -class A { context fun foo(x: Int) } -class B { fun foo(x: Object) } - -// here 'foo' resolves to the one in 'A' -// because both 'A' and 'B' are in the same scope -// but the one from 'A' is more specific -context(A) fun B.example1() = foo(5) -``` ## Q&A about design decisions -*Q: Why introduce the concept of contextual visibility?* - -One of the main objections to the previous design was the potential scope pollution. Although named context parameters make this issue less pressing, there are still problems with bringing a whole context receiver into implicit scope. +*Q: Why drop context receivers?* -In particular, some functions like `toString`, `equals`, ... become ambiguous — or even worse, they resolve to something different from what the developer intended. Since they are declared without context, those functions won't be accessible from context receivers. +One of the main objections to the previous design was the potential scope pollution: -*Q: Why only give the author of a callable the ability to add an empty context, and as a result make that callable available from a context receiver?* +- Having too many functions available in implicit scope makes it difficult to find the right one; +- It becomes much harder to establish where a certain member is coming from. -We've toyed with several designs here (annotation in the declaration, annotation in the import), but named context parameter + `with` seems to work just fine, and it doesn't bring any new piece to the mix. +We think that context parameters provide a better first step in understanding how implicit context resolution fits in Kotlin, without that caveat. *Q: Why do callable references resolve differently with context parameters and extension receivers?* -We've actually considered five different designs; there are the reason we've decided for the described one. +We've considered five different designs; these are the reasons we've decided for the described one. * Even though extension (and dispatch) receivers are implicit in function calls, they are explicit in callable references — think of `User::save`. This is not the case for context parameters. * We found the use case of passing a reference to a function such `map`, while reusing the context from the outer declaration, a quite important one. @@ -546,14 +443,14 @@ One important piece of feedback in the previous design was the need for function fun context(a: A, b: B, block: context(A, B) () -> R): R ``` -The subtyping restriction would disallow this case, since `A` and `B` may (potentially) be subtypes of one another. We've considered adding some sort of additional restrictions at the type level or making this function a built-in, but in the end, we've decided that it's the developer's responsibility to keep a clean and ordered context. An ambiguity error seems reasonable if the problem really arises at the use site. +The subtyping restriction would disallow this case, since `A` and `B` may (potentially) be subtypes of one another. We've considered adding some sort of additional restrictions at the type level or making this function a built-in, but in the end, we've decided that it's the developer's responsibility to keep a clean and ordered context. An ambiguity error seems reasonable if the problem arises at the use site. *Q: Why drop the context-in-class feature altogether?* -As we explored the design, we concluded that the interplay between contexts and inheritance and visibility is quite complex. Think of questions such as whether a context receiver to a class should also be in implicit scope in an extension method to that class, and whether that should depend on the potential visibility of such context receiver. At this point, we think that a good answer would only come if we fully design "`with`` properties", that is, values which also enter the context scope in their block. +As we explored the design, we concluded that the interplay between contexts and inheritance and visibility is quite complex. Think of questions such as whether a context parameter to a class should also be in implicit scope in an extension method to that class, and whether that should depend on the potential visibility of such context parameter. At this point, we think that a good answer would only come if we fully design "`with`` properties", that is, values that also enter the context scope in their block. -An additional stone in the way is that one commonly requested use case is to have a constructor with value parameters, and other where some of those are contextual. However, this leads to platform clashes, so we would need an additional design on that part. +An additional stone in the way is that one commonly requested use case is to have a constructor with value parameters and another where some of those are contextual. However, this leads to platform clashes, so we would need an additional design on that part. -## Acknowledgements +## Acknowledgments -We thank everybody who has contributed to improving and growing the context receiver proposal to this current form. Several people have devoted a significant amount of time being interviewed and sharing their thoughts with us, showing the involvement of the Kotlin community in this process. +We thank everybody who has contributed to improving and growing the context parameters proposal to this current form. Several people have devoted a significant amount of time to being interviewed and sharing their thoughts with us, showing the involvement of the Kotlin community in this process. From d6bdc74f3fd8b76f3d7f4b68bea5a4b9c5e3b2e7 Mon Sep 17 00:00:00 2001 From: Alejandro Serrano Date: Wed, 24 Jan 2024 12:26:16 +0100 Subject: [PATCH 07/28] Function applicability --- proposals/context-parameters.md | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/proposals/context-parameters.md b/proposals/context-parameters.md index fa65e1f34..a4e29b491 100644 --- a/proposals/context-parameters.md +++ b/proposals/context-parameters.md @@ -15,7 +15,7 @@ This document is not (yet) formally a KEEP, since it lacks some of the technical 1. Introduction of named context parameters, 2. Context receivers are dropped, -3. Removal of `this@Type` syntax, introduction of `context()`, +3. Removal of `this@Type` syntax, introduction of `implicit()`, 4. Contexts are not allowed in constructors, 5. Callable references resolve their context arguments eagerly, 6. Context-in-classes are dropped. @@ -98,7 +98,7 @@ Note that, like in the case of extension receivers, those types are considered e fun withConsoleLogger(block: context(Logger) () -> A) = ... withConsoleLogger { - val logger = context() + val logger = implicit() // you can call functions with Logger as context parameter logWithTime("doing something") } @@ -116,10 +116,10 @@ fun context(a: A, b: B, block: context(A, B) () -> R): R = block(a, b) fun context(a: A, b: B, c: C, block: context(A, B, C) () -> R): R = block(a, b, c) ``` -**§8** *(`context` function)*: We also provide a generic way to obtain a value by type from the context. It allows access to context parameters even when declared using `_`, or within the body of a lambda. +**§8** *(`implicit` function)*: We also provide a generic way to obtain a value by type from the context. It allows access to context parameters even when declared using `_`, or within the body of a lambda. ```kotlin -context(ctx: A) fun context(): A = ctx +context(ctx: A) fun implicit(): A = ctx ``` This function replaces the uses of `this@Type` in the previous iteration of the design. @@ -376,12 +376,14 @@ functionContext: 'context' [ '(' [ receiverType { ',' receiverType } ] ') ] **§23** *(declaration with context parameters)*: The context parameters declared for a callable are available in the same way as "regular" value parameters in the body of the function. Both value and context parameters are introduced in the same scope, there is no shadowing between them. -**§24** *(most specific candidate)*: When choosing the **most specific candidate** we follow the Kotlin specification, with one addition: +**§24** *(function applicability)*: Building the constraint system is modified for lambda arguments. Compared with the [Kotlin specification](https://kotlinlang.org/spec/overload-resolution.html#description), the type of the parameter _Um_ is replaced with _nocontext(Um)_, where _nocontext_ removes the initial `context` block from the function type. + +**§25** *(most specific candidate)*: When choosing the **most specific candidate** we follow the Kotlin specification, with one addition: * Candidates with context parameters are considered more specific than those without them. * But there is no other prioritization coming from the length of the context parameter list or their types. -**§25** *(context resolution)*: Once the overload candidate is chosen, we **resolve** context parameters (if any). For each context parameter: +**§26** *(context resolution)*: Once the overload candidate is chosen, we **resolve** context parameters (if any). For each context parameter: * We traverse the tower of scopes looking for **exactly one** default receiver or context parameter with a compatible type. * Anonymous context parameters (declared with `_`) also participate in this process. @@ -410,7 +412,7 @@ context(console: ConsoleLogger, file: FileLogger) fun example3() = ### JVM ABI and Java compatibility -**§26** *(JVM and Java compatibility)*: In the JVM a function with context parameters is represented as a regular function with the context parameters situated at the *beginning* of the parameter list. The name of the context parameter, if present, is used as the name of the parameter. +**§27** *(JVM and Java compatibility)*: In the JVM a function with context parameters is represented as a regular function with the context parameters situated at the *beginning* of the parameter list. The name of the context parameter, if present, is used as the name of the parameter. * Note that parameter names do not impact JVM ABI compatibility. From 83b835c70ec2594945195c3808c1b78932686595 Mon Sep 17 00:00:00 2001 From: Alejandro Serrano Date: Fri, 26 Jan 2024 10:51:09 +0100 Subject: [PATCH 08/28] More technical updates --- proposals/context-parameters.md | 50 ++++++++++++++++++++++++--------- 1 file changed, 37 insertions(+), 13 deletions(-) diff --git a/proposals/context-parameters.md b/proposals/context-parameters.md index a4e29b491..c2f313f7d 100644 --- a/proposals/context-parameters.md +++ b/proposals/context-parameters.md @@ -40,6 +40,7 @@ This document is not (yet) formally a KEEP, since it lacks some of the technical * [Technical design](#technical-design) * [Syntax](#syntax) * [Extended resolution algorithm](#extended-resolution-algorithm) + * [Extended type inference algorithm](#extended-type-inference-algorithm) * [JVM ABI and Java compatibility](#jvm-abi-and-java-compatibility) * [Q\&A about design decisions](#qa-about-design-decisions) * [Acknowledgments](#acknowledgments) @@ -52,6 +53,14 @@ This document is not (yet) formally a KEEP, since it lacks some of the technical * Within the body of the declared member, the value of the context parameter is accessible using its name, similar to value parameters. * It is allowed to use `_` as a name; in that case, the value is not accessible through any name (but still participates in context resolution). +```kotlin +context(analysisScope: AnalysisScope) +fun Type.equalTo(other: Type): Boolean = ... + +context(analysisScope: AnalysisScope) +val Type.isNullable: Boolean get() = ... +``` + **§2** *(restrictions)*: * It is an *error* to declare an **empty** list of context parameters. @@ -106,7 +115,7 @@ withConsoleLogger { ## Standard library support -**§7** *(`context` function)*: To extend the implicit scope in a contextual manner we provide additional functions in the standard library. +**§8** *(`context` function)*: To extend the implicit scope in a contextual manner we provide additional functions in the standard library. * The implementation may be built into the compiler, instead of having a plethora of functions defined in the standard library. @@ -116,7 +125,7 @@ fun context(a: A, b: B, block: context(A, B) () -> R): R = block(a, b) fun context(a: A, b: B, c: C, block: context(A, B, C) () -> R): R = block(a, b, c) ``` -**§8** *(`implicit` function)*: We also provide a generic way to obtain a value by type from the context. It allows access to context parameters even when declared using `_`, or within the body of a lambda. +**§9** *(`implicit` function)*: We also provide a generic way to obtain a value by type from the context. It allows access to context parameters even when declared using `_`, or within the body of a lambda. ```kotlin context(ctx: A) fun implicit(): A = ctx @@ -173,7 +182,7 @@ We do this by introducing a **bridge function** that simply wraps the access to context(r: Raise) inline fun raise(error: Error): Nothing = r.raise(error) ``` -**§12** *(receiver migration, members)*: If your library exposes a "scope" or "context" type, we suggest moving to context parameters: +**§13** *(receiver migration, members)*: If your library exposes a "scope" or "context" type, we suggest moving to context parameters: 1. functions with the scope type as extension receiver should be refactored to use context parameters, 2. operations defined as members and extending other types should be taken out of the interface definition, if possible, @@ -376,19 +385,17 @@ functionContext: 'context' [ '(' [ receiverType { ',' receiverType } ] ') ] **§23** *(declaration with context parameters)*: The context parameters declared for a callable are available in the same way as "regular" value parameters in the body of the function. Both value and context parameters are introduced in the same scope, there is no shadowing between them. -**§24** *(function applicability)*: Building the constraint system is modified for lambda arguments. Compared with the [Kotlin specification](https://kotlinlang.org/spec/overload-resolution.html#description), the type of the parameter _Um_ is replaced with _nocontext(Um)_, where _nocontext_ removes the initial `context` block from the function type. +**§24** *(applicability, lambdas)*: Building the constraint system is modified for lambda arguments. Compared with the [Kotlin specification](https://kotlinlang.org/spec/overload-resolution.html#description), the type of the parameter _Um_ is replaced with _nocontext(Um)_, where _nocontext_ removes the initial `context` block from the function type. -**§25** *(most specific candidate)*: When choosing the **most specific candidate** we follow the Kotlin specification, with one addition: +**§25** *(applicability, context resolution)*: After the first phase of function applicability -- checking the type constraint problem -- an additional **context resolution** phase is inserted. For each potentially applicable callable, for each context parameter, we traverse the tower of scopes looking for **exactly one** default receiver or context parameter with a compatible type. -* Candidates with context parameters are considered more specific than those without them. -* But there is no other prioritization coming from the length of the context parameter list or their types. +There are three possible outcomes of this process: -**§26** *(context resolution)*: Once the overload candidate is chosen, we **resolve** context parameters (if any). For each context parameter: +1. If _no_ compatible context value is found for at least one context parameter, then the call is _not_ applicable, and it is removed from the candidate set as a result. +2. If for at least one context parameter there is more than one compatible value at the same level (and case 1 does not apply), a _context ambiguity_ error is issued. +3. If none of (1) or (2) apply, then the candidate is applicable. -* We traverse the tower of scopes looking for **exactly one** default receiver or context parameter with a compatible type. - * Anonymous context parameters (declared with `_`) also participate in this process. -* It is an ambiguity error if more than one value is found at the same level. -* It is an overload resolution error if no applicable value is found. +The following piece of code exemplifies how scoping interacts with context resolution: ```kotlin interface Logger { @@ -410,9 +417,26 @@ context(console: ConsoleLogger, file: FileLogger) fun example3() = with(console) { logWithTime("hello") } // no ambiguity, uses 'console' ``` +**§26** *(applicability, `DslMarker`)*: During context resolution, if at a certain scope there is a potential contextual value in scope (either coming from a context parameter or for implicit scope) marked with an annotation `@X` which is itself annotated with [`@DslMarker`](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-dsl-marker/) then: + +- It is an _error_ for two such values to be available in the same scope. +- If context resolution chooses a contextual value with the same annotation, but in an outer scope, it is a compilation _error_. +- If a call binds to a receiver with the same annotation, it is a compilation _error_. + +These rules extend the usual behavior of `@DslMarker` to cover both receivers and context parameters uniformly. + +**§27** *(most specific candidate)*: When choosing the **most specific candidate** we follow the [Kotlin specification](https://kotlinlang.org/spec/overload-resolution.html#choosing-the-most-specific-candidate-from-the-overload-candidate-set), with one addition: + +* Candidates with context parameters are considered more specific than those without them. +* But there is no other prioritization coming from the length of the context parameter list or their types. + +### Extended type inference algorithm + +**§28** *(lambda literal inference)*: the type inference process in the [Kotlin specification](https://kotlinlang.org/spec/type-inference.html#statements-with-lambda-literals) should take context parameters into account. Note that unless a function type with context is "pushed" as a type for the lambda, context parameters are never inferred. + ### JVM ABI and Java compatibility -**§27** *(JVM and Java compatibility)*: In the JVM a function with context parameters is represented as a regular function with the context parameters situated at the *beginning* of the parameter list. The name of the context parameter, if present, is used as the name of the parameter. +**§29** *(JVM and Java compatibility)*: In the JVM a function with context parameters is represented as a regular function with the context parameters situated at the *beginning* of the parameter list. The name of the context parameter, if present, is used as the name of the parameter. * Note that parameter names do not impact JVM ABI compatibility. From 9b80eb5e2b3044040f4d902d99f975a7297ef152 Mon Sep 17 00:00:00 2001 From: Alejandro Serrano Date: Fri, 26 Jan 2024 12:27:35 +0100 Subject: [PATCH 09/28] Word of caution --- proposals/context-parameters.md | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/proposals/context-parameters.md b/proposals/context-parameters.md index c2f313f7d..8d17fee26 100644 --- a/proposals/context-parameters.md +++ b/proposals/context-parameters.md @@ -264,6 +264,21 @@ Context parameters lift two of the main restrictions of this mode of use: **§D** *(context-oriented dispatch use case)*: Context parameters can be used to simulate functions available for a type, by requiring an interface that uses the type as an argument. This is very similar to type classes in other languages. +```kotlin +interface ToJson { + fun toJson(thing: T): Json +} + +context(serializer: ToJson) fun T.toJson(): Json = + serializer.toJson(this) + +context(_: ToJson) fun ApplicationCall.json(thing: T) = + this.respondJson(thing.toJson()) +``` + +We strongly advise _against_ creating "copies" of an API but with an added context parameter. +Overload resolution is tricky, and the developer may end up calling the unintended version. + ```kotlin interface Comparator { fun compareTo(one: T, other: T): Boolean From 998cab54418408173dc9f4ffb1e7fac8cba011f5 Mon Sep 17 00:00:00 2001 From: Alejandro Serrano Date: Mon, 29 Jan 2024 15:39:45 +0100 Subject: [PATCH 10/28] Apply suggestions --- proposals/context-parameters.md | 41 +++++++++++++++++++++------------ 1 file changed, 26 insertions(+), 15 deletions(-) diff --git a/proposals/context-parameters.md b/proposals/context-parameters.md index 8d17fee26..ab1040c6c 100644 --- a/proposals/context-parameters.md +++ b/proposals/context-parameters.md @@ -66,7 +66,7 @@ val Type.isNullable: Boolean get() = ... * It is an *error* to declare an **empty** list of context parameters. * It is an *error* if the **name** of a context parameter **coincides** with the name of another context or value parameter to the callable (except for multiple uses of `_`). -**§3** *(implicitness)*: When calling a member with context parameters, those are not spelled out. Rather, the value for each of those arguments is **resolved** from two sources: in-scope context parameters, and implicit scope. We say that context parameters are **implicit**. +**§3** *(implicitness)*: When calling a member with context parameters, those are not spelled out. Rather, the value for each of those arguments is **resolved** from two sources: in-scope context parameters, and implicit receivers ([as defined by the Kotlin specification](https://kotlinlang.org/spec/overload-resolution.html#receivers)). We say that context parameters are **implicit**. ```kotlin context(logger: Logger) fun logWithTime(message: String) = @@ -86,7 +86,7 @@ context(logger: Logger) fun User.doAction() { **§5** *(naming ambiguity)*: We use the term **context** with two meanings: 1. For a declaration, it refers to the collection of context parameters declared in its signature. We also use the term *contextual function or property*. -2. Within a body, we use context to refer to the combination of the implicit scope and context parameters. This context is the source for context resolution, that is, for "filling in" the implicit context parameters. +2. Within a block, we use context to refer to the combination of implicit receivers and context parameters in scope in that block. This context is the source for context resolution, that is, for "filling in" the implicit context parameters. **§6** *(function types)*: **Function types** are extended with context parameters. It is only allowed to mention the *type* of context parameters, names are not supported. @@ -97,11 +97,20 @@ context(Transaction) (UserId) -> User? context(Logger) User.() -> Int ``` -Note that, like in the case of extension receivers, those types are considered equivalent (for typing purposes, **not** for resolution purposes) to the function types in which all parameters are declared as value parameters. +Note that, like in the case of extension receivers, those types are considered equivalent (for typing purposes, **not** for resolution purposes) to the function types in which all parameters are declared as value parameters _in the same order_. + +```kotlin +// these are all equivalent types +context(Logger, User) () -> Int +context(Logger) User.() -> Int +context(Logger) (User) -> Int +Logger.(User) -> Int +(Logger, User) -> Int +``` **§7** *(lambdas)*: If a lambda is assigned a function type with context parameters, those behave as if declared with `_` as its name. -* They participate in context resolution but are only accessible through the `context` function (defined below). +* They participate in context resolution but are only accessible through the `implicit` function (defined below). ```kotlin fun withConsoleLogger(block: context(Logger) () -> A) = ... @@ -115,7 +124,7 @@ withConsoleLogger { ## Standard library support -**§8** *(`context` function)*: To extend the implicit scope in a contextual manner we provide additional functions in the standard library. +**§8** *(`context` function)*: The `context` function adds a new value to the context, in an anonymous manner. * The implementation may be built into the compiler, instead of having a plethora of functions defined in the standard library. @@ -368,7 +377,7 @@ context(users: UserService) fun saveAll(users: List): Unit = **§19** *(no contexts in constructors, workaround)*: Note that Kotlin is very restrictive with constructors, as it doesn't allow them to be `suspend` either; the same workarounds (companion object + `invoke``, function with the name of the class) are available in this case. -**§20** *(no contexts in constructors, future)*: We have defined four levels of how this support may pan out in the future: +**§20** *(no contexts in constructors, future)*: We have defined levels of increasing support for contexts in classes, which steer how the feature may evolve: 1. No context for constructors (current one), 2. Contexts only for secondary constructors, @@ -388,10 +397,10 @@ context(users: UserService) fun saveAll(users: List): Unit = functionModifier: ... | context propertyModifier: ... | context -context: 'context' [ '(' [ parameter { ',' parameter } ] ')' ] +context: 'context' '(' parameter { ',' parameter } [ ',' ] ')' -functionType: [ functionContext ] {NL} [ receiverType {NL} '.' {NL} ] ... -functionContext: 'context' [ '(' [ receiverType { ',' receiverType } ] ') ] +functionType: [ functionContext ] [ receiverType '.' ] ... +functionContext: 'context' '(' receiverType { ',' receiverType } [ ',' ] ')' ``` **Recommended style:** annotations, context parameters, other modifiers as per the [usual style guide](https://kotlinlang.org/docs/coding-conventions.html#modifiers-order). @@ -432,7 +441,7 @@ context(console: ConsoleLogger, file: FileLogger) fun example3() = with(console) { logWithTime("hello") } // no ambiguity, uses 'console' ``` -**§26** *(applicability, `DslMarker`)*: During context resolution, if at a certain scope there is a potential contextual value in scope (either coming from a context parameter or for implicit scope) marked with an annotation `@X` which is itself annotated with [`@DslMarker`](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-dsl-marker/) then: +**§26** *(applicability, `DslMarker`)*: During context resolution, if at a certain scope there is a potential contextual value in scope (either coming from a context parameter or from an implicit receiver) marked with an annotation `@X` which is itself annotated with [`@DslMarker`](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-dsl-marker/) then: - It is an _error_ for two such values to be available in the same scope. - If context resolution chooses a contextual value with the same annotation, but in an outer scope, it is a compilation _error_. @@ -451,10 +460,12 @@ These rules extend the usual behavior of `@DslMarker` to cover both receivers an ### JVM ABI and Java compatibility -**§29** *(JVM and Java compatibility)*: In the JVM a function with context parameters is represented as a regular function with the context parameters situated at the *beginning* of the parameter list. The name of the context parameter, if present, is used as the name of the parameter. - -* Note that parameter names do not impact JVM ABI compatibility. +**§29** *(JVM and Java compatibility)*: In the JVM a function with context parameters is represented as a function with additional parameters. In particular, the order is: +1. Context parameters, if present; +2. Extension receiver, if present; +3. Regular value parameters. +* Note that parameter names do not impact JVM ABI compatibility, but we use the names given in parameter declarations as far as possible. ## Q&A about design decisions @@ -462,7 +473,7 @@ These rules extend the usual behavior of `@DslMarker` to cover both receivers an One of the main objections to the previous design was the potential scope pollution: -- Having too many functions available in implicit scope makes it difficult to find the right one; +- Having too many functions available in scope without qualification makes it difficult to find the right one; - It becomes much harder to establish where a certain member is coming from. We think that context parameters provide a better first step in understanding how implicit context resolution fits in Kotlin, without that caveat. @@ -488,7 +499,7 @@ The subtyping restriction would disallow this case, since `A` and `B` may (poten *Q: Why drop the context-in-class feature altogether?* -As we explored the design, we concluded that the interplay between contexts and inheritance and visibility is quite complex. Think of questions such as whether a context parameter to a class should also be in implicit scope in an extension method to that class, and whether that should depend on the potential visibility of such context parameter. At this point, we think that a good answer would only come if we fully design "`with`` properties", that is, values that also enter the context scope in their block. +As we explored the design, we concluded that the interplay between contexts and inheritance and visibility is quite complex. Think of questions such as whether a context parameter to a class should also be in the context of an extension method to that class, and whether that should depend on the potential visibility of such context parameter. At this point, we think that a good answer would only come if we fully design "`with`` properties", that is, values that also enter the context scope in their block. An additional stone in the way is that one commonly requested use case is to have a constructor with value parameters and another where some of those are contextual. However, this leads to platform clashes, so we would need an additional design on that part. From d1f6b4b01fb565f32ffdbf172bcb074ac9adf7c2 Mon Sep 17 00:00:00 2001 From: Alejandro Serrano Date: Wed, 31 Jan 2024 10:45:15 +0100 Subject: [PATCH 11/28] Implement suggestions --- proposals/context-parameters.md | 129 ++++++++++++++++++++------------ 1 file changed, 82 insertions(+), 47 deletions(-) diff --git a/proposals/context-parameters.md b/proposals/context-parameters.md index ab1040c6c..748d41226 100644 --- a/proposals/context-parameters.md +++ b/proposals/context-parameters.md @@ -7,7 +7,7 @@ ## Abstract -This is an updated proposal for [KEEP-259](https://github.com/Kotlin/KEEP/issues/259), formerly known as _context receivers_. The new design addresses the issues raised by the users of the prototype and across the community. +This is an updated proposal for [KEEP-259](https://github.com/Kotlin/KEEP/issues/259), formerly known as _context receivers_. The new design addresses the issues raised by the users of the prototype implementing that previous iteration of the design and across the community at large. This document is not (yet) formally a KEEP, since it lacks some of the technical elements. Those are going to be provided at a later time, but we thought it would be interesting to open the discussion even if the design is not fully formalized. @@ -48,7 +48,7 @@ This document is not (yet) formally a KEEP, since it lacks some of the technical ## Members with context parameters -**§1** *(declaration)*: Every callable member (functions — but not constructors — and properties) gets additional support for **context parameters**. Context parameters are declared with the `context` keyword followed by a list of parameters, each of the form `name: Type`. +**§1.1** *(declaration)*: Every callable member (functions — but not constructors — and properties) gets additional support for **context parameters**. Context parameters are declared with the `context` keyword followed by a list of parameters, each of the form `name: Type`. * Within the body of the declared member, the value of the context parameter is accessible using its name, similar to value parameters. * It is allowed to use `_` as a name; in that case, the value is not accessible through any name (but still participates in context resolution). @@ -61,12 +61,12 @@ context(analysisScope: AnalysisScope) val Type.isNullable: Boolean get() = ... ``` -**§2** *(restrictions)*: +**§1.2** *(restrictions)*: * It is an *error* to declare an **empty** list of context parameters. * It is an *error* if the **name** of a context parameter **coincides** with the name of another context or value parameter to the callable (except for multiple uses of `_`). -**§3** *(implicitness)*: When calling a member with context parameters, those are not spelled out. Rather, the value for each of those arguments is **resolved** from two sources: in-scope context parameters, and implicit receivers ([as defined by the Kotlin specification](https://kotlinlang.org/spec/overload-resolution.html#receivers)). We say that context parameters are **implicit**. +**§1.3** *(implicitness)*: When calling a member with context parameters, those are not spelled out. Rather, the value for each of those arguments is **resolved** from two sources: in-scope context parameters, and implicit receivers ([as defined by the Kotlin specification](https://kotlinlang.org/spec/overload-resolution.html#receivers)). We say that context parameters are **implicit**. ```kotlin context(logger: Logger) fun logWithTime(message: String) = @@ -78,17 +78,17 @@ context(logger: Logger) fun User.doAction() { } ``` -**§4** *(override)*: Context parameters are part of the signature, and follow the same rules as regular value parameters concerning overriding: +**§1.4** *(override)*: Context parameters are part of the signature, and follow the same rules as regular value parameters concerning overriding: * When overriding, the type and order of context parameters must coincide. * It is allowed (yet discouraged) to change the name of a context parameter. -**§5** *(naming ambiguity)*: We use the term **context** with two meanings: +**§1.5** *(naming ambiguity)*: We use the term **context** with two meanings: 1. For a declaration, it refers to the collection of context parameters declared in its signature. We also use the term *contextual function or property*. 2. Within a block, we use context to refer to the combination of implicit receivers and context parameters in scope in that block. This context is the source for context resolution, that is, for "filling in" the implicit context parameters. -**§6** *(function types)*: **Function types** are extended with context parameters. It is only allowed to mention the *type* of context parameters, names are not supported. +**§1.6** *(function types)*: **Function types** are extended with context parameters. It is only allowed to mention the *type* of context parameters, names are not supported. * We do not want to inspect the body of a lambda looking for different context parameter names during overload resolution. The reasoning is similar to how we restrict lambdas with no declared arguments to be 0 or 1-ary. @@ -108,7 +108,7 @@ Logger.(User) -> Int (Logger, User) -> Int ``` -**§7** *(lambdas)*: If a lambda is assigned a function type with context parameters, those behave as if declared with `_` as its name. +**§1.7** *(lambdas)*: If a lambda is assigned a function type with context parameters, those behave as if declared with `_` as its name. * They participate in context resolution but are only accessible through the `implicit` function (defined below). @@ -116,15 +116,16 @@ Logger.(User) -> Int fun withConsoleLogger(block: context(Logger) () -> A) = ... withConsoleLogger { - val logger = implicit() // you can call functions with Logger as context parameter logWithTime("doing something") + // you can use 'implicit' to access the context parameter + implicit().log("hello") } ``` ## Standard library support -**§8** *(`context` function)*: The `context` function adds a new value to the context, in an anonymous manner. +**§2.1** *(`context` function)*: The `context` function adds a new value to the context, in an anonymous manner. * The implementation may be built into the compiler, instead of having a plethora of functions defined in the standard library. @@ -134,7 +135,7 @@ fun context(a: A, b: B, block: context(A, B) () -> R): R = block(a, b) fun context(a: A, b: B, c: C, block: context(A, B, C) () -> R): R = block(a, b, c) ``` -**§9** *(`implicit` function)*: We also provide a generic way to obtain a value by type from the context. It allows access to context parameters even when declared using `_`, or within the body of a lambda. +**§2.2** *(`implicit` function)*: We also provide a generic way to obtain a value by type from the context. It allows access to context parameters even when declared using `_`, or within the body of a lambda. ```kotlin context(ctx: A) fun implicit(): A = ctx @@ -144,7 +145,7 @@ This function replaces the uses of `this@Type` in the previous iteration of the ### Reflection -**§10** *(callable reflection)*: The following additions to the `kotlin.reflect` are required for information about members. +**§2.3** *(callable reflection)*: The following additions to the `kotlin.reflect` are required for information about members. ```kotlin interface KParameter { @@ -161,13 +162,13 @@ val KCallable<*>.contextParameters: List ``` -**§11** *(property reflection)*: Properties with context parameters are not `KProperty0`, `1`, nor `2`, but rather simply `KProperty`. +**§2.4** *(property reflection)*: Properties with context parameters are not `KProperty0`, `1`, nor `2`, but rather simply `KProperty`. ## Simulating receivers There are cases where the need to refer to members of a context parameter through its name may hurt readability — this happens mostly in relation to DSLs. In this section, we provide guidance on how to solve this issue. -**§12** *(bridge function)*: Given an interface with some members, +**§3.1** *(bridge function)*: Given an interface with some members, ```kotlin interface Raise { @@ -191,7 +192,7 @@ We do this by introducing a **bridge function** that simply wraps the access to context(r: Raise) inline fun raise(error: Error): Nothing = r.raise(error) ``` -**§13** *(receiver migration, members)*: If your library exposes a "scope" or "context" type, we suggest moving to context parameters: +**§3.2** *(receiver migration, members)*: If your library exposes a "scope" or "context" type, we suggest moving to context parameters: 1. functions with the scope type as extension receiver should be refactored to use context parameters, 2. operations defined as members and extending other types should be taken out of the interface definition, if possible, @@ -214,7 +215,7 @@ interface Raise { context(_: Raise) fun Either.bind() = ... ``` -**§13** *(receiver migration, run functions)*: We advise keeping any function taking a lambda where the scope type appears as extension receiver as is, and provide a new variant with a context parameter. The latter must unfortunately get a different name to prevent overload conflicts. +**§3.3** *(receiver migration, run functions)*: We advise keeping any function taking a lambda where the scope type appears as extension receiver as is, and provide a new variant with a context parameter. The latter must unfortunately get a different name to prevent overload conflicts. ```kotlin fun runRaise(block: Raise.() -> A): Either @@ -222,7 +223,7 @@ fun runRaise(block: Raise.() -> A): Either fun runRaiseContext(block: context(Raise) () -> A): Either ``` -**§14** *(receiver migration, source compatibility)*: The rules above guarantee source compatibility for users of the interface. +**§3.4** *(receiver migration, source compatibility)*: The rules above guarantee source compatibility for users of the interface. ## Use cases @@ -230,7 +231,7 @@ This is a recollection and categorization of the different use cases we have fou ### As implicits -**§A** *(implicit use case)*: In this case, the context parameter is thought of as a set of services available to a piece of code, but without the ceremony of passing those services explicitly in every call. In most cases, those contexts are introduced with an explicit name. +**§4.1** *(implicit use case)*: In this case, the context parameter is thought of as a set of services available to a piece of code, but without the ceremony of passing those services explicitly in every call. In most cases, those contexts are introduced with an explicit name. A `Repository` class for a particular entity or a `Logger` are good examples of this mode of use. @@ -240,7 +241,7 @@ context(users: UserRepository) fun User.getFriends() = ... ### As scopes -**§B** *(scope use case)*: In this case we use the context parameter as a marker of being inside a particular scope, which unlocks additional abilities. A prime example is `CoroutineScope`, which adds `launch`, `async`, and so on. In this mode of use: +**§4.2** *(scope use case)*: In this case we use the context parameter as a marker of being inside a particular scope, which unlocks additional abilities. A prime example is `CoroutineScope`, which adds `launch`, `async`, and so on. In this mode of use: * The `Scope` type is carefully designed, and the functions are exposed using bridge functions, * In most cases, the context parameter has no name or is irrelevant. @@ -256,7 +257,7 @@ context(_: ResourceScope) fun File.open(): InputStream ### For extending DSLs -**§C** *(DSLs use case)*: In this case, contexts are used to provide new members available in a domain-specific language. Currently, this is approached by declaring an interface that represents the "DSL context", and then having member or extension functions on that interface. +**§4.3** *(DSLs use case)*: In this case, contexts are used to provide new members available in a domain-specific language. Currently, this is approached by declaring an interface that represents the "DSL context", and then having member or extension functions on that interface. ```kotlin interface HtmlScope { @@ -271,7 +272,7 @@ Context parameters lift two of the main restrictions of this mode of use: ### Context-oriented dispatch / externally-implemented interface / type classes -**§D** *(context-oriented dispatch use case)*: Context parameters can be used to simulate functions available for a type, by requiring an interface that uses the type as an argument. This is very similar to type classes in other languages. +**§4.4** *(context-oriented dispatch use case)*: Context parameters can be used to simulate functions available for a type, by requiring an interface that uses the type as an argument. This is very similar to type classes in other languages. ```kotlin interface ToJson { @@ -302,27 +303,27 @@ context(_: Comparator) fun max(x: T, y: T) = ### Dependency injection -**§E.1** *(dependency injection use case)*: You can view context parameters as values that must be "injected" from the context for the code to work. Since context arguments are completely resolved at compile-time, this provides something like dependency injection in the language. +**§4.5.1** *(dependency injection use case)*: You can view context parameters as values that must be "injected" from the context for the code to work. Since context arguments are completely resolved at compile-time, this provides something like dependency injection in the language. -**§E.1** *(dependency injection use case, companion object)*: In some cases, you may want to define that instantiating a certain class requires some value — this creates the typical hierarchy of dependency we see in DI frameworks. We can accomplish this by faking a constructor with those contexts: +**§4.5.2** *(dependency injection use case, companion object)*: In some cases, you may want to define that instantiating a certain class requires some value — this creates the typical hierarchy of dependency we see in DI frameworks. We can accomplish this by faking a constructor with those contexts: ```kotlin interface Logger { ... } interface UserService { ... } -class DbUserService(val logger: Logger, val connection: DbConnection) { +class DbUserService(val logger: Logger, val connection: DbConnection): UserService { companion object { - context(logger: Logger, connection: Connection) + context(logger: Logger, connection: DbConnection) operator fun invoke(): DbUserService = DbUserService(logger, connection) } } ``` -**§E.2** *(dependency injection use case, discouragement)*: Note that we suggest against this feature, since having parameters explicitly reduces the need to nest too many `context` calls. +**§4.5.3** *(dependency injection use case, discouragement)*: Note that we suggest against this feature, since having parameters explicitly reduces the need to nest too many `context` calls. ```kotlin // do not do this -context(ConsoleLogger(), ConnectionPool(2)) { +context(ConsoleLogger(), DbConnectionPool(2)) { context(DbUserService()) { ... } @@ -330,7 +331,7 @@ context(ConsoleLogger(), ConnectionPool(2)) { // better be explicit about object creation val logger = ConsoleLogger() -val pool = ConnectionPool(2) +val pool = DbConnectionPool(2) val userService = DbUserService(logger, pool) // and then inject everything you need in one go context(logger, userService) { @@ -340,7 +341,7 @@ context(logger, userService) { ## Callable references -**§15** *(callable references, eager resolution)*: References to callables declared with context parameters are resolved **eagerly**: +**§5.1** *(callable references, eager resolution)*: References to callables declared with context parameters are resolved **eagerly**: * The required context parameters must be resolved in the context in which the reference is created, * The resulting type does not mention context. @@ -361,7 +362,7 @@ context(users: UserService) fun example() { } ``` -**§16** *(callable references, motivation)*: This design was motivated by the pattern of having a set of functions sharing a common context parameter, like in the example below. +**§5.2** *(callable references, motivation)*: This design was motivated by the pattern of having a set of functions sharing a common context parameter, like in the example below. ```kotlin context(users: UserService) fun save(u: User): Unit { ... } @@ -369,15 +370,15 @@ context(users: UserService) fun saveAll(users: List): Unit = users.forEach(::save) // ::save is resolved as (User) -> Unit ``` -**§17** *(callable references, future)*: We consider as **future** improvement a more complex resolution of callables, in which the context is taken into account when the callable is used as an argument of a function that expects a function type with context. +**§5.3** *(callable references, future)*: We consider as **future** improvement a more complex resolution of callables, in which the context is taken into account when the callable is used as an argument of a function that expects a function type with context. ## Context and classes -**§18** *(no contexts in constructors)*: We do **not** support context parameters in constructor declarations (neither primary nor secondary). There are some issues around their design, especially when mixed with inheritance and private/protected visibility. +**§6.1** *(no contexts in constructors)*: We do **not** support context parameters in constructor declarations (neither primary nor secondary). There are some issues around their design, especially when mixed with inheritance and private/protected visibility. -**§19** *(no contexts in constructors, workaround)*: Note that Kotlin is very restrictive with constructors, as it doesn't allow them to be `suspend` either; the same workarounds (companion object + `invoke``, function with the name of the class) are available in this case. +**§6.2** *(no contexts in constructors, workaround)*: Note that Kotlin is very restrictive with constructors, as it doesn't allow them to be `suspend` either; the same workarounds (companion object + `invoke``, function with the name of the class) are available in this case. -**§20** *(no contexts in constructors, future)*: We have defined levels of increasing support for contexts in classes, which steer how the feature may evolve: +**§6.3** *(no contexts in constructors, future)*: We have defined levels of increasing support for contexts in classes, which steer how the feature may evolve: 1. No context for constructors (current one), 2. Contexts only for secondary constructors, @@ -385,13 +386,13 @@ context(users: UserService) fun saveAll(users: List): Unit = 4. Support for `val`/`var` for context parameter, but without entering the context, 5. Context parameters declared with `val`/`var` enter the context of every declaration. -**§21** *(no contexts in class declarations)*: The prototype also contains a "context in class" feature, which both adds a context parameter to the constructor and makes that value available implicitly in the body of the class. We've explored some possibilities, but the conclusion was that we do not know at this point which is the right one. Furthermore, we think that "scoped properties" may bring a better overall solution to this problem; and adding this feature now would get in the way. +**§6.4** *(no contexts in class declarations)*: At this point, we include no "context in class" feature, which would both add a context parameter to the constructor and make that value available implicitly in the body of the class. We've explored some possibilities, but the conclusion was that we do not know at this point which is the right one. Furthermore, we think that "scoped properties" may bring a better overall solution to this problem; and adding this feature now would get in the way. ## Technical design ### Syntax -**§22** *(`context` is a modifier)*: Everybody's favorite topic! Although the current implementation places some restrictions on the location of the context block, the intention is to turn it (syntactically) into a modifier. In terms of the Kotlin grammar, +**§7.1** *(`context` is a modifier)*: Everybody's favorite topic! Although the current implementation places some restrictions on the location of the context block, the intention is to turn it (syntactically) into a modifier. In terms of the Kotlin grammar, ``` functionModifier: ... | context @@ -407,11 +408,11 @@ functionContext: 'context' '(' receiverType { ',' receiverType } [ ',' ] ')' ### Extended resolution algorithm -**§23** *(declaration with context parameters)*: The context parameters declared for a callable are available in the same way as "regular" value parameters in the body of the function. Both value and context parameters are introduced in the same scope, there is no shadowing between them. +**§7.2** *(declaration with context parameters)*: The context parameters declared for a callable are available in the same way as "regular" value parameters in the body of the function. Both value and context parameters are introduced in the same scope, there is no shadowing between them (remember that parameters names must be unique across both value and context parameters), -**§24** *(applicability, lambdas)*: Building the constraint system is modified for lambda arguments. Compared with the [Kotlin specification](https://kotlinlang.org/spec/overload-resolution.html#description), the type of the parameter _Um_ is replaced with _nocontext(Um)_, where _nocontext_ removes the initial `context` block from the function type. +**§7.3** *(applicability, lambdas)*: Building the constraint system is modified for lambda arguments. Compared with the [Kotlin specification](https://kotlinlang.org/spec/overload-resolution.html#description), the type of the parameter _Um_ is replaced with _nocontext(Um)_, where _nocontext_ removes the initial `context` block from the function type. -**§25** *(applicability, context resolution)*: After the first phase of function applicability -- checking the type constraint problem -- an additional **context resolution** phase is inserted. For each potentially applicable callable, for each context parameter, we traverse the tower of scopes looking for **exactly one** default receiver or context parameter with a compatible type. +**§7.4** *(applicability, context resolution)*: After the first phase of function applicability -- checking the type constraint problem -- an additional **context resolution** phase is inserted. For each potentially applicable callable, for each context parameter, we traverse the tower of scopes looking for **exactly one** default receiver or context parameter with a compatible type. There are three possible outcomes of this process: @@ -433,15 +434,19 @@ context(logger: Logger) fun logWithTime(message: String) = ... context(console: ConsoleLogger, file: FileLogger) fun example1() = logWithTime("hello") // ambiguity error -context(file: FileLogger) fun example2() = context(ConsoleLogger()) { - logWithTime("hello") // no ambiguity, uses the new ConsoleLogger -} +context(file: FileLogger) fun example2() = + context(ConsoleLogger()) { + logWithTime("hello") // no ambiguity, uses the new ConsoleLogger + } context(console: ConsoleLogger, file: FileLogger) fun example3() = + context(console) { logWithTime("hello") } // no ambiguity, uses 'console' + +context(console: ConsoleLogger, file: FileLogger) fun example4() = with(console) { logWithTime("hello") } // no ambiguity, uses 'console' ``` -**§26** *(applicability, `DslMarker`)*: During context resolution, if at a certain scope there is a potential contextual value in scope (either coming from a context parameter or from an implicit receiver) marked with an annotation `@X` which is itself annotated with [`@DslMarker`](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-dsl-marker/) then: +**§7.5** *(applicability, `DslMarker`)*: During context resolution, if at a certain scope there is a potential contextual value in scope (either coming from a context parameter or from an implicit receiver) marked with an annotation `@X` which is itself annotated with [`@DslMarker`](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-dsl-marker/) then: - It is an _error_ for two such values to be available in the same scope. - If context resolution chooses a contextual value with the same annotation, but in an outer scope, it is a compilation _error_. @@ -449,23 +454,53 @@ context(console: ConsoleLogger, file: FileLogger) fun example3() = These rules extend the usual behavior of `@DslMarker` to cover both receivers and context parameters uniformly. -**§27** *(most specific candidate)*: When choosing the **most specific candidate** we follow the [Kotlin specification](https://kotlinlang.org/spec/overload-resolution.html#choosing-the-most-specific-candidate-from-the-overload-candidate-set), with one addition: +```kotlin +@DslMarker annotation class ExampleMarker + +@ExampleMarker class ExampleScope { + fun exemplify(): A +} + +fun withExampleReceiver(value: A, block: ExampleScope.() -> T): T = ... +fun withExampleContext(value: A, block: context(example: ExampleScope) () -> T): T = + withExampleReceiver(value) { block() } + +fun dslMarkerExample() = + withExample(3) { + withExampleReceiver("b") { + // at this point you can only use + // the ExampleScope introduced in "b" + // to resolve context parameters + + withExample(true) { + // at this point you can only use + // the ExampleScope introduced in "c" + // to resolve context parameters + + // the receiver from "b" is also inaccessible + this.exemplify() // rejected: DSL scope violation + } + } + } +``` + +**§7.6** *(most specific candidate)*: When choosing the **most specific candidate** we follow the [Kotlin specification](https://kotlinlang.org/spec/overload-resolution.html#choosing-the-most-specific-candidate-from-the-overload-candidate-set), with one addition: * Candidates with context parameters are considered more specific than those without them. * But there is no other prioritization coming from the length of the context parameter list or their types. ### Extended type inference algorithm -**§28** *(lambda literal inference)*: the type inference process in the [Kotlin specification](https://kotlinlang.org/spec/type-inference.html#statements-with-lambda-literals) should take context parameters into account. Note that unless a function type with context is "pushed" as a type for the lambda, context parameters are never inferred. +**§7.7** *(lambda literal inference)*: the type inference process in the [Kotlin specification](https://kotlinlang.org/spec/type-inference.html#statements-with-lambda-literals) should take context parameters into account. Note that unless a function type with context is "pushed" as a type for the lambda, context parameters are never inferred. ### JVM ABI and Java compatibility -**§29** *(JVM and Java compatibility)*: In the JVM a function with context parameters is represented as a function with additional parameters. In particular, the order is: +**§7.8** *(JVM and Java compatibility)*: In the JVM a function with context parameters is represented as a function with additional parameters. In particular, the order is: 1. Context parameters, if present; 2. Extension receiver, if present; 3. Regular value parameters. -* Note that parameter names do not impact JVM ABI compatibility, but we use the names given in parameter declarations as far as possible. +Note that parameter names do not impact JVM ABI compatibility, but we use the names given in parameter declarations as far as possible. ## Q&A about design decisions From f44427b9269c36ebcd14855049ca5fda7c7bdf7f Mon Sep 17 00:00:00 2001 From: Alejandro Serrano Date: Wed, 31 Jan 2024 13:58:08 +0100 Subject: [PATCH 12/28] Properties --- proposals/context-parameters.md | 36 ++++++++++++++++++++++++--------- 1 file changed, 26 insertions(+), 10 deletions(-) diff --git a/proposals/context-parameters.md b/proposals/context-parameters.md index 748d41226..fd5fb1a3d 100644 --- a/proposals/context-parameters.md +++ b/proposals/context-parameters.md @@ -66,7 +66,21 @@ val Type.isNullable: Boolean get() = ... * It is an *error* to declare an **empty** list of context parameters. * It is an *error* if the **name** of a context parameter **coincides** with the name of another context or value parameter to the callable (except for multiple uses of `_`). -**§1.3** *(implicitness)*: When calling a member with context parameters, those are not spelled out. Rather, the value for each of those arguments is **resolved** from two sources: in-scope context parameters, and implicit receivers ([as defined by the Kotlin specification](https://kotlinlang.org/spec/overload-resolution.html#receivers)). We say that context parameters are **implicit**. +**§1.3** *(properties)*: Properties declared with context parameters may **not** declare an _initializer_, nor use _delegation_. + +```kotlin +// not allowed (property with initializer) +context(users: UserRepository) +val firstUser: User? = users.getById(1) + +// allowed +context(users: UserRepository) +val firstUser: User? get() = users.getById(1) +``` + +The underlying reason is that the value for the context parameter is not available until the property is accessed, and may change according to the context. + +**§1.4** *(implicitness)*: When calling a member with context parameters, those are not spelled out. Rather, the value for each of those arguments is **resolved** from two sources: in-scope context parameters, and implicit receivers ([as defined by the Kotlin specification](https://kotlinlang.org/spec/overload-resolution.html#receivers)). We say that context parameters are **implicit**. ```kotlin context(logger: Logger) fun logWithTime(message: String) = @@ -78,17 +92,17 @@ context(logger: Logger) fun User.doAction() { } ``` -**§1.4** *(override)*: Context parameters are part of the signature, and follow the same rules as regular value parameters concerning overriding: +**§1.5** *(override)*: Context parameters are part of the signature, and follow the same rules as regular value parameters concerning overriding: -* When overriding, the type and order of context parameters must coincide. +* The type and order of context parameters must coincide. * It is allowed (yet discouraged) to change the name of a context parameter. -**§1.5** *(naming ambiguity)*: We use the term **context** with two meanings: +**§1.6** *(naming ambiguity)*: We use the term **context** with two meanings: 1. For a declaration, it refers to the collection of context parameters declared in its signature. We also use the term *contextual function or property*. 2. Within a block, we use context to refer to the combination of implicit receivers and context parameters in scope in that block. This context is the source for context resolution, that is, for "filling in" the implicit context parameters. -**§1.6** *(function types)*: **Function types** are extended with context parameters. It is only allowed to mention the *type* of context parameters, names are not supported. +**§1.7** *(function types)*: **Function types** are extended with context parameters. It is only allowed to mention the *type* of context parameters, names are not supported. * We do not want to inspect the body of a lambda looking for different context parameter names during overload resolution. The reasoning is similar to how we restrict lambdas with no declared arguments to be 0 or 1-ary. @@ -108,7 +122,7 @@ Logger.(User) -> Int (Logger, User) -> Int ``` -**§1.7** *(lambdas)*: If a lambda is assigned a function type with context parameters, those behave as if declared with `_` as its name. +**§1.8** *(lambdas)*: If a lambda is assigned a function type with context parameters, those behave as if declared with `_` as its name. * They participate in context resolution but are only accessible through the `implicit` function (defined below). @@ -157,7 +171,7 @@ interface KParameter { val KCallable<*>.contextParameters: List get() = parameters.filter { - it.kind == KParameter.Kind.CONTEXT_RECEIVER + it.kind == KParameter.Kind.CONTEXT_PARAMETER } ``` @@ -376,7 +390,7 @@ context(users: UserService) fun saveAll(users: List): Unit = **§6.1** *(no contexts in constructors)*: We do **not** support context parameters in constructor declarations (neither primary nor secondary). There are some issues around their design, especially when mixed with inheritance and private/protected visibility. -**§6.2** *(no contexts in constructors, workaround)*: Note that Kotlin is very restrictive with constructors, as it doesn't allow them to be `suspend` either; the same workarounds (companion object + `invoke``, function with the name of the class) are available in this case. +**§6.2** *(no contexts in constructors, workaround)*: Note that Kotlin is very restrictive with constructors, as it doesn't allow them to be `suspend` either; the same workarounds (companion object + `invoke`, function with the name of the class) are available in this case. **§6.3** *(no contexts in constructors, future)*: We have defined levels of increasing support for contexts in classes, which steer how the feature may evolve: @@ -495,13 +509,15 @@ fun dslMarkerExample() = ### JVM ABI and Java compatibility -**§7.8** *(JVM and Java compatibility)*: In the JVM a function with context parameters is represented as a function with additional parameters. In particular, the order is: +**§7.8** *(JVM and Java compatibility, functions)*: In the JVM a function with context parameters is represented as a function with additional parameters. In particular, the order is: 1. Context parameters, if present; 2. Extension receiver, if present; 3. Regular value parameters. Note that parameter names do not impact JVM ABI compatibility, but we use the names given in parameter declarations as far as possible. +**§7.9** *(JVM and Java compatibility, properties)*: In the JVM a property with context parameters is represented by its corresponding getter and/or setter. This representation follows the same argument order described in §7.8. + ## Q&A about design decisions *Q: Why drop context receivers?* @@ -534,7 +550,7 @@ The subtyping restriction would disallow this case, since `A` and `B` may (poten *Q: Why drop the context-in-class feature altogether?* -As we explored the design, we concluded that the interplay between contexts and inheritance and visibility is quite complex. Think of questions such as whether a context parameter to a class should also be in the context of an extension method to that class, and whether that should depend on the potential visibility of such context parameter. At this point, we think that a good answer would only come if we fully design "`with`` properties", that is, values that also enter the context scope in their block. +As we explored the design, we concluded that the interplay between contexts and inheritance and visibility is quite complex. Think of questions such as whether a context parameter to a class should also be in the context of an extension method to that class, and whether that should depend on the potential visibility of such context parameter. At this point, we think that a good answer would only come if we fully design "`with` properties", that is, values that also enter the context scope in their block. An additional stone in the way is that one commonly requested use case is to have a constructor with value parameters and another where some of those are contextual. However, this leads to platform clashes, so we would need an additional design on that part. From d6d82c0b04bfd8d2d424d7296ce85c3fea465466 Mon Sep 17 00:00:00 2001 From: Alejandro Serrano Date: Thu, 1 Feb 2024 13:46:24 +0100 Subject: [PATCH 13/28] More examples about DslMarker --- proposals/context-parameters.md | 21 ++++++++++++++------- 1 file changed, 14 insertions(+), 7 deletions(-) diff --git a/proposals/context-parameters.md b/proposals/context-parameters.md index fd5fb1a3d..36f235b06 100644 --- a/proposals/context-parameters.md +++ b/proposals/context-parameters.md @@ -471,7 +471,7 @@ These rules extend the usual behavior of `@DslMarker` to cover both receivers an ```kotlin @DslMarker annotation class ExampleMarker -@ExampleMarker class ExampleScope { +@ExampleMarker interface ExampleScope { fun exemplify(): A } @@ -479,20 +479,27 @@ fun withExampleReceiver(value: A, block: ExampleScope.() -> T): T = .. fun withExampleContext(value: A, block: context(example: ExampleScope) () -> T): T = withExampleReceiver(value) { block() } +context(ExampleScope) fun similarExampleTo(other: A): A = ... + fun dslMarkerExample() = - withExample(3) { - withExampleReceiver("b") { + withExampleContext(3) { // (1) + withExampleReceiver("b") { // (2) // at this point you can only use - // the ExampleScope introduced in "b" + // the ExampleScope introduced (2) // to resolve context parameters - withExample(true) { + similarExampleTo("hello") // correct, uses (2) + similarExampleTo(1) // rejected: DSL scope violation + // since it resolved to (1) + + withExampleContext(true) { // (3) // at this point you can only use - // the ExampleScope introduced in "c" + // the ExampleScope introduced (3) // to resolve context parameters - // the receiver from "b" is also inaccessible this.exemplify() // rejected: DSL scope violation + // since it binds the receiver from (2) + similarExampleTo("bye") // rejected: DSL scope violation } } } From f3a5782f32b42822f447b4ee4c844a5170bf3edb Mon Sep 17 00:00:00 2001 From: Alejandro Serrano Date: Mon, 19 Feb 2024 14:04:20 +0100 Subject: [PATCH 14/28] Implement suggestions --- proposals/context-parameters.md | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/proposals/context-parameters.md b/proposals/context-parameters.md index 36f235b06..1d06fc692 100644 --- a/proposals/context-parameters.md +++ b/proposals/context-parameters.md @@ -142,6 +142,7 @@ withConsoleLogger { **§2.1** *(`context` function)*: The `context` function adds a new value to the context, in an anonymous manner. * The implementation may be built into the compiler, instead of having a plethora of functions defined in the standard library. +* Implementations are encouraged, but not required, to mark these functions as `inline`. ```kotlin fun context(context: A, block: context(A) () -> R): R = block(context) @@ -151,11 +152,13 @@ fun context(a: A, b: B, c: C, block: context(A, B, C) () -> R): R = **§2.2** *(`implicit` function)*: We also provide a generic way to obtain a value by type from the context. It allows access to context parameters even when declared using `_`, or within the body of a lambda. +* Implementations are encouraged, but not required, to mark this function as `inline`. + ```kotlin context(ctx: A) fun implicit(): A = ctx ``` -This function replaces the uses of `this@Type` in the previous iteration of the design. +_Note:_ This function replaces the uses of `this@Type` in the previous iteration of the design. ### Reflection @@ -200,7 +203,7 @@ context(_: Raise) fun Either.bind(): A = } ``` -We do this by introducing a **bridge function** that simply wraps the access to the context parameter. +We do this by introducing a **bridge function** at top level that simply wraps the access to the context parameter. ```kotlin context(r: Raise) inline fun raise(error: Error): Nothing = r.raise(error) @@ -271,7 +274,7 @@ context(_: ResourceScope) fun File.open(): InputStream ### For extending DSLs -**§4.3** *(DSLs use case)*: In this case, contexts are used to provide new members available in a domain-specific language. Currently, this is approached by declaring an interface that represents the "DSL context", and then having member or extension functions on that interface. +**§4.3.1** *(DSLs use case)*: In this case, contexts are used to provide new members available in a domain-specific language. Currently, this is approached by declaring an interface that represents the "DSL context", and then having member or extension functions on that interface. ```kotlin interface HtmlScope { @@ -284,6 +287,8 @@ Context parameters lift two of the main restrictions of this mode of use: * It's possible to add new members with an extension receiver without modifying the Scope class itself. * It's possible to add members which are only available when the DSL Scope has certain type arguments. +**§4.3.1** *(`DslMarker`)*: we strive to make `@DslMarker` annotations work uniformly across receivers and context parameters, as described in §7.5. + ### Context-oriented dispatch / externally-implemented interface / type classes **§4.4** *(context-oriented dispatch use case)*: Context parameters can be used to simulate functions available for a type, by requiring an interface that uses the type as an argument. This is very similar to type classes in other languages. From baa2141f636d8a1a27d94945966fa16031b8a2d7 Mon Sep 17 00:00:00 2001 From: Alejandro Serrano Date: Tue, 5 Mar 2024 08:52:10 +0100 Subject: [PATCH 15/28] Update proposals/context-parameters.md Co-authored-by: Osip Fatkullin --- proposals/context-parameters.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/proposals/context-parameters.md b/proposals/context-parameters.md index 1d06fc692..1ad39efec 100644 --- a/proposals/context-parameters.md +++ b/proposals/context-parameters.md @@ -287,7 +287,7 @@ Context parameters lift two of the main restrictions of this mode of use: * It's possible to add new members with an extension receiver without modifying the Scope class itself. * It's possible to add members which are only available when the DSL Scope has certain type arguments. -**§4.3.1** *(`DslMarker`)*: we strive to make `@DslMarker` annotations work uniformly across receivers and context parameters, as described in §7.5. +**§4.3.2** *(`DslMarker`)*: we strive to make `@DslMarker` annotations work uniformly across receivers and context parameters, as described in §7.5. ### Context-oriented dispatch / externally-implemented interface / type classes From 96894616a6cccba11907bd0146cb95514d7120d1 Mon Sep 17 00:00:00 2001 From: Alejandro Serrano Date: Tue, 5 Mar 2024 08:56:03 +0100 Subject: [PATCH 16/28] Apply suggestions from @ilya-g --- proposals/context-parameters.md | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/proposals/context-parameters.md b/proposals/context-parameters.md index 1ad39efec..6441179b7 100644 --- a/proposals/context-parameters.md +++ b/proposals/context-parameters.md @@ -25,7 +25,7 @@ This document is not (yet) formally a KEEP, since it lacks some of the technical * [Abstract](#abstract) * [Summary of changes from the previous proposal](#summary-of-changes-from-the-previous-proposal) * [Table of contents](#table-of-contents) -* [Members with context parameters](#members-with-context-parameters) +* [Declarations with context parameters](#declarations-with-context-parameters) * [Standard library support](#standard-library-support) * [Reflection](#reflection) * [Simulating receivers](#simulating-receivers) @@ -46,11 +46,11 @@ This document is not (yet) formally a KEEP, since it lacks some of the technical * [Acknowledgments](#acknowledgments) -## Members with context parameters +## Declarations with context parameters -**§1.1** *(declaration)*: Every callable member (functions — but not constructors — and properties) gets additional support for **context parameters**. Context parameters are declared with the `context` keyword followed by a list of parameters, each of the form `name: Type`. +**§1.1** *(declaration)*: Every callable declaration (functions — but not constructors — and properties) gets additional support for **context parameters**. Context parameters are declared with the `context` keyword followed by a list of parameters, each of the form `name: Type`. -* Within the body of the declared member, the value of the context parameter is accessible using its name, similar to value parameters. +* Within the body of the declaration, the value of the context parameter is accessible using its name, similar to value parameters. * It is allowed to use `_` as a name; in that case, the value is not accessible through any name (but still participates in context resolution). ```kotlin @@ -80,7 +80,7 @@ val firstUser: User? get() = users.getById(1) The underlying reason is that the value for the context parameter is not available until the property is accessed, and may change according to the context. -**§1.4** *(implicitness)*: When calling a member with context parameters, those are not spelled out. Rather, the value for each of those arguments is **resolved** from two sources: in-scope context parameters, and implicit receivers ([as defined by the Kotlin specification](https://kotlinlang.org/spec/overload-resolution.html#receivers)). We say that context parameters are **implicit**. +**§1.4** *(implicitness)*: When calling a function or property with context parameters, those are not spelled out. Rather, the value for each of those arguments is **resolved** from two sources: in-scope context parameters, and implicit receivers ([as defined by the Kotlin specification](https://kotlinlang.org/spec/overload-resolution.html#receivers)). We say that context parameters are **implicit**. ```kotlin context(logger: Logger) fun logWithTime(message: String) = @@ -162,7 +162,7 @@ _Note:_ This function replaces the uses of `this@Type` in the previous iteration ### Reflection -**§2.3** *(callable reflection)*: The following additions to the `kotlin.reflect` are required for information about members. +**§2.3** *(callable reflection)*: The following additions to the `kotlin.reflect` are required for information about context parameters. ```kotlin interface KParameter { From bdc08ff60dab8344f2bb94cdf6b06184cd15072b Mon Sep 17 00:00:00 2001 From: Alejandro Serrano Date: Mon, 11 Mar 2024 11:49:53 +0100 Subject: [PATCH 17/28] Further clarifications --- proposals/context-parameters.md | 33 ++++++++++++++++++++++++++++++--- 1 file changed, 30 insertions(+), 3 deletions(-) diff --git a/proposals/context-parameters.md b/proposals/context-parameters.md index 6441179b7..56297efe3 100644 --- a/proposals/context-parameters.md +++ b/proposals/context-parameters.md @@ -54,11 +54,16 @@ This document is not (yet) formally a KEEP, since it lacks some of the technical * It is allowed to use `_` as a name; in that case, the value is not accessible through any name (but still participates in context resolution). ```kotlin +interface Type { + context(analysisScope: AnalysisScope) + val isNullable: Boolean = ... +} + context(analysisScope: AnalysisScope) fun Type.equalTo(other: Type): Boolean = ... -context(analysisScope: AnalysisScope) -val Type.isNullable: Boolean get() = ... +context(_: AnalysisScope) +val Type.isBoolean: Boolean = this.equalTo(BuiltIns.Boolean) ``` **§1.2** *(restrictions)*: @@ -405,7 +410,29 @@ context(users: UserService) fun saveAll(users: List): Unit = 4. Support for `val`/`var` for context parameter, but without entering the context, 5. Context parameters declared with `val`/`var` enter the context of every declaration. -**§6.4** *(no contexts in class declarations)*: At this point, we include no "context in class" feature, which would both add a context parameter to the constructor and make that value available implicitly in the body of the class. We've explored some possibilities, but the conclusion was that we do not know at this point which is the right one. Furthermore, we think that "scoped properties" may bring a better overall solution to this problem; and adding this feature now would get in the way. +**§6.4** *(no contexts in class declarations)*: At this point, we include no "context in class" feature, which would both add a context parameter to the constructor and make that value available implicitly in the body of the class. We've explored some possibilities, but the conclusion was that we do not know at this point which is the right one. + +In the code above we show a piece of code not allowed by the current design, and two different ways we can provide a similar behavior using constructs that are available. + +```kotlin +// this was allowed in the previous iteration +context(AnalysisScope) class TypeScope { + fun Type.equalTo(other: Type): Boolean = ... +} + +// possibility 1: scope stored once for the entire lifetime of an instance +class TypeScope(val analysisScope: AnalysisScope) { + fun Type.equalTo(other: Type): Boolean = ... +} + +// possibility 2: scope required on each operation +class TypeScope { + context(analysisScope: AnalysisScope) + fun Type.equalTo(other: Type): Boolean = ... +} +``` + +Furthermore, we think that "scoped properties" may bring a better overall solution to this problem; and adding this feature now would get in the way. ## Technical design From 8269f456c07d714315788f057b8597c0e877c2ed Mon Sep 17 00:00:00 2001 From: Alejandro Serrano Date: Thu, 21 Mar 2024 10:58:06 +0100 Subject: [PATCH 18/28] Clarifications --- proposals/context-parameters.md | 41 ++++++++++++++++++++++++++++----- 1 file changed, 35 insertions(+), 6 deletions(-) diff --git a/proposals/context-parameters.md b/proposals/context-parameters.md index 56297efe3..69e3755c9 100644 --- a/proposals/context-parameters.md +++ b/proposals/context-parameters.md @@ -41,7 +41,7 @@ This document is not (yet) formally a KEEP, since it lacks some of the technical * [Syntax](#syntax) * [Extended resolution algorithm](#extended-resolution-algorithm) * [Extended type inference algorithm](#extended-type-inference-algorithm) - * [JVM ABI and Java compatibility](#jvm-abi-and-java-compatibility) + * [ABI compatibility](#abi-compatibility) * [Q\&A about design decisions](#qa-about-design-decisions) * [Acknowledgments](#acknowledgments) @@ -71,7 +71,7 @@ val Type.isBoolean: Boolean = this.equalTo(BuiltIns.Boolean) * It is an *error* to declare an **empty** list of context parameters. * It is an *error* if the **name** of a context parameter **coincides** with the name of another context or value parameter to the callable (except for multiple uses of `_`). -**§1.3** *(properties)*: Properties declared with context parameters may **not** declare an _initializer_, nor use _delegation_. +**§1.3** *(properties)*: Properties declared with context parameters may **not** declare an _initializer_, nor use _delegation_. It is **not** possible to declare context parameters for the getter or setter. ```kotlin // not allowed (property with initializer) @@ -97,11 +97,13 @@ context(logger: Logger) fun User.doAction() { } ``` -**§1.5** *(override)*: Context parameters are part of the signature, and follow the same rules as regular value parameters concerning overriding: +**§1.5** *(override and overloading)*: Context parameters are part of the signature, and follow the same rules as regular value parameters concerning overriding: * The type and order of context parameters must coincide. * It is allowed (yet discouraged) to change the name of a context parameter. +It is a conflict to declare overloads which only differ in the order of the context parameters. + **§1.6** *(naming ambiguity)*: We use the term **context** with two meanings: 1. For a declaration, it refers to the collection of context parameters declared in its signature. We also use the term *contextual function or property*. @@ -167,7 +169,21 @@ _Note:_ This function replaces the uses of `this@Type` in the previous iteration ### Reflection -**§2.3** *(callable reflection)*: The following additions to the `kotlin.reflect` are required for information about context parameters. +**§2.3** *(callable references)*: As described in the corresponding section below, callable references eagerly resolve all the context parameters, so no changes are needed to the API when working with those. + +```kotlin +context(users: UserService) val User.name: String = ... + +context(users: UserService) fun doSomething() { + val nameProperty = User::name + // type is KProperty1 + // the context 'users' is already resolved in the reference +} +``` + +The following two paragraphs describe the extensions required when contextual declarations are accessed via [reflection](https://kotlinlang.org/docs/reflection.html); usually via the [`kotlin.reflect` package](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin.reflect/). For example, when using the `declaredMembers` property from a `KClass`. + +**§2.4** *(callable reflection)*: We extend the `kotlin.reflect` API to with information about context parameters. ```kotlin interface KParameter { @@ -184,7 +200,7 @@ val KCallable<*>.contextParameters: List ``` -**§2.4** *(property reflection)*: Properties with context parameters are not `KProperty0`, `1`, nor `2`, but rather simply `KProperty`. +**§2.5** *(property reflection)*: Properties with context parameters are represented as extending `KProperty` and also the function type corresponding to their getter. At this point, we do not extend the hierarchy of `KProperty0/1/2` to account for the additional context parameters. ## Simulating receivers @@ -542,11 +558,22 @@ fun dslMarkerExample() = * Candidates with context parameters are considered more specific than those without them. * But there is no other prioritization coming from the length of the context parameter list or their types. +For example, the following call to `foo` is declared ambiguous, since `"hello"` may work both as `String` or `Any` context parameter. + +```kotlin +context(Any) fun foo() {} +context(String) fun foo() {} + +fun test() = with("hello") { + foo() +} +``` + ### Extended type inference algorithm **§7.7** *(lambda literal inference)*: the type inference process in the [Kotlin specification](https://kotlinlang.org/spec/type-inference.html#statements-with-lambda-literals) should take context parameters into account. Note that unless a function type with context is "pushed" as a type for the lambda, context parameters are never inferred. -### JVM ABI and Java compatibility +### ABI compatibility **§7.8** *(JVM and Java compatibility, functions)*: In the JVM a function with context parameters is represented as a function with additional parameters. In particular, the order is: 1. Context parameters, if present; @@ -557,6 +584,8 @@ Note that parameter names do not impact JVM ABI compatibility, but we use the na **§7.9** *(JVM and Java compatibility, properties)*: In the JVM a property with context parameters is represented by its corresponding getter and/or setter. This representation follows the same argument order described in §7.8. +**§7.10** *(other targets)*: Targets may not follow the same ABI compatibility guarantees as those described for the JVM. + ## Q&A about design decisions *Q: Why drop context receivers?* From db8fecc0d0e38c198c18807250d3649e8a1ef24e Mon Sep 17 00:00:00 2001 From: Alejandro Serrano Date: Fri, 22 Mar 2024 16:47:53 +0100 Subject: [PATCH 19/28] Clairifications --- proposals/context-parameters.md | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/proposals/context-parameters.md b/proposals/context-parameters.md index 69e3755c9..62f76ff26 100644 --- a/proposals/context-parameters.md +++ b/proposals/context-parameters.md @@ -48,8 +48,9 @@ This document is not (yet) formally a KEEP, since it lacks some of the technical ## Declarations with context parameters -**§1.1** *(declaration)*: Every callable declaration (functions — but not constructors — and properties) gets additional support for **context parameters**. Context parameters are declared with the `context` keyword followed by a list of parameters, each of the form `name: Type`. +**§1.1** *(declaration)*: Function and property declarations gets support for **context parameters**. Context parameters are declared with the `context` keyword followed by a list of parameters, each of the form `name: Type`. +* Constructors may **not** declare context parameters. * Within the body of the declaration, the value of the context parameter is accessible using its name, similar to value parameters. * It is allowed to use `_` as a name; in that case, the value is not accessible through any name (but still participates in context resolution). @@ -118,7 +119,7 @@ context(Transaction) (UserId) -> User? context(Logger) User.() -> Int ``` -Note that, like in the case of extension receivers, those types are considered equivalent (for typing purposes, **not** for resolution purposes) to the function types in which all parameters are declared as value parameters _in the same order_. +Note that, like in the case of extension receivers, those types are [considered equivalent (for typing purposes, **not** for resolution purposes)](https://kotlinlang.org/spec/type-system.html#function-types) to the function types in which all parameters are declared as value parameters _in the same order_. ```kotlin // these are all equivalent types @@ -214,7 +215,7 @@ interface Raise { } ``` -we want to let users call `raise` whenever `Raise` is in scope, without having to mention the context parameter, but ensuring that the corresponding value is part of the context. +the authors of the API want to let users call `raise` whenever `Raise` is in scope, without having to mention the context parameter, but ensuring that the corresponding value is part of the context. ```kotlin context(_: Raise) fun Either.bind(): A = @@ -224,13 +225,13 @@ context(_: Raise) fun Either.bind(): A = } ``` -We do this by introducing a **bridge function** at top level that simply wraps the access to the context parameter. +They can do this by introducing a **bridge function** at top level that simply wraps the access to the context parameter. ```kotlin context(r: Raise) inline fun raise(error: Error): Nothing = r.raise(error) ``` -**§3.2** *(receiver migration, members)*: If your library exposes a "scope" or "context" type, we suggest moving to context parameters: +**§3.2** *(receiver migration, members)*: If a library exposes a "scope" or "context" type, we suggest reworking the API to use context parameters: 1. functions with the scope type as extension receiver should be refactored to use context parameters, 2. operations defined as members and extending other types should be taken out of the interface definition, if possible, From 560752f0c36514b574a878eb5606fbc968fe4615 Mon Sep 17 00:00:00 2001 From: Alejandro Serrano Date: Fri, 22 Mar 2024 17:54:30 +0100 Subject: [PATCH 20/28] Clarification --- proposals/context-parameters.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/proposals/context-parameters.md b/proposals/context-parameters.md index 62f76ff26..36901ac99 100644 --- a/proposals/context-parameters.md +++ b/proposals/context-parameters.md @@ -570,6 +570,8 @@ fun test() = with("hello") { } ``` +The reasoning for this particular rule is that, since contexts are implicit, there is no way for the user to resolve to the other function if required. This implicitness also means that it's harder to figure out which overload is called from the program code -- once again, there's no value you can easily point to. In contrast, in the case of an explicit parameter, you can always use `f("hello" as Any)` to force using `(Any) -> R` over `(String) -> R`. + ### Extended type inference algorithm **§7.7** *(lambda literal inference)*: the type inference process in the [Kotlin specification](https://kotlinlang.org/spec/type-inference.html#statements-with-lambda-literals) should take context parameters into account. Note that unless a function type with context is "pushed" as a type for the lambda, context parameters are never inferred. From c3382e48bdc74ce9ab340d06226d34d6a0fcf7b7 Mon Sep 17 00:00:00 2001 From: Leonid Startsev Date: Mon, 25 Mar 2024 13:05:25 +0000 Subject: [PATCH 21/28] fixup! Clarification --- proposals/context-parameters.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/proposals/context-parameters.md b/proposals/context-parameters.md index 36901ac99..1d09fc3cb 100644 --- a/proposals/context-parameters.md +++ b/proposals/context-parameters.md @@ -48,7 +48,7 @@ This document is not (yet) formally a KEEP, since it lacks some of the technical ## Declarations with context parameters -**§1.1** *(declaration)*: Function and property declarations gets support for **context parameters**. Context parameters are declared with the `context` keyword followed by a list of parameters, each of the form `name: Type`. +**§1.1** *(declaration)*: Function and property declarations get support for **context parameters**. Context parameters are declared with the `context` keyword followed by a list of parameters, each of the form `name: Type`. * Constructors may **not** declare context parameters. * Within the body of the declaration, the value of the context parameter is accessible using its name, similar to value parameters. From e35eb6de422655f74ccec6854bc589c858ed15b2 Mon Sep 17 00:00:00 2001 From: Alejandro Serrano Date: Wed, 27 Mar 2024 11:24:14 +0100 Subject: [PATCH 22/28] Remove unneeded inline --- proposals/context-parameters.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/proposals/context-parameters.md b/proposals/context-parameters.md index 1d09fc3cb..6f8d826b5 100644 --- a/proposals/context-parameters.md +++ b/proposals/context-parameters.md @@ -228,7 +228,7 @@ context(_: Raise) fun Either.bind(): A = They can do this by introducing a **bridge function** at top level that simply wraps the access to the context parameter. ```kotlin -context(r: Raise) inline fun raise(error: Error): Nothing = r.raise(error) +context(r: Raise) fun raise(error: Error): Nothing = r.raise(error) ``` **§3.2** *(receiver migration, members)*: If a library exposes a "scope" or "context" type, we suggest reworking the API to use context parameters: From 915d5cdfdbbb8bff92a43f2830de9f6c87b3ef96 Mon Sep 17 00:00:00 2001 From: Alejandro Serrano Date: Thu, 28 Mar 2024 10:01:51 +0100 Subject: [PATCH 23/28] Builder inference + syntax disambiguation --- proposals/context-parameters.md | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/proposals/context-parameters.md b/proposals/context-parameters.md index 6f8d826b5..a8b91c648 100644 --- a/proposals/context-parameters.md +++ b/proposals/context-parameters.md @@ -467,7 +467,17 @@ functionType: [ functionContext ] [ receiverType '.' ] ... functionContext: 'context' '(' receiverType { ',' receiverType } [ ',' ] ')' ``` -**Recommended style:** annotations, context parameters, other modifiers as per the [usual style guide](https://kotlinlang.org/docs/coding-conventions.html#modifiers-order). +**Recommended style:** annotations, context parameters, then other modifiers as per the [usual style guide](https://kotlinlang.org/docs/coding-conventions.html#modifiers-order). + +**§7.1bis** *(disambiguating `context`)*: We do not enforce `context` as a hard keyword, leading to potential ambiguities in the body of declarations. In particular, `context(` may be both the beginning of: + +1. A call to a function named `context`, +2. The context parameter list of a local declaration. + +The compiler should perform a lookahead of two additional tokens to disambiguate: + +- If the opening parenthesis is followed by an identifier and a ':', then it should be treated as the beginning of a local declaration. +- Otherwise, it should be treated as the beginning of a function call. ### Extended resolution algorithm @@ -576,6 +586,8 @@ The reasoning for this particular rule is that, since contexts are implicit, the **§7.7** *(lambda literal inference)*: the type inference process in the [Kotlin specification](https://kotlinlang.org/spec/type-inference.html#statements-with-lambda-literals) should take context parameters into account. Note that unless a function type with context is "pushed" as a type for the lambda, context parameters are never inferred. +Context parameters take part in [builder-style type inference](https://kotlinlang.org/spec/type-inference.html#builder-style-type-inference), or any similar process defined by the implementation and whose goal is to infer better types for receivers. + ### ABI compatibility **§7.8** *(JVM and Java compatibility, functions)*: In the JVM a function with context parameters is represented as a function with additional parameters. In particular, the order is: From f361a154b0292f77c4b9bd3021cc85d289da1b60 Mon Sep 17 00:00:00 2001 From: Alejandro Serrano Date: Thu, 28 Mar 2024 10:49:16 +0100 Subject: [PATCH 24/28] Better explanation of properties --- proposals/context-parameters.md | 21 ++++++++++++++++----- 1 file changed, 16 insertions(+), 5 deletions(-) diff --git a/proposals/context-parameters.md b/proposals/context-parameters.md index a8b91c648..e48469356 100644 --- a/proposals/context-parameters.md +++ b/proposals/context-parameters.md @@ -72,19 +72,30 @@ val Type.isBoolean: Boolean = this.equalTo(BuiltIns.Boolean) * It is an *error* to declare an **empty** list of context parameters. * It is an *error* if the **name** of a context parameter **coincides** with the name of another context or value parameter to the callable (except for multiple uses of `_`). -**§1.3** *(properties)*: Properties declared with context parameters may **not** declare an _initializer_, nor use _delegation_. It is **not** possible to declare context parameters for the getter or setter. +**§1.3** *(properties)*: As mentioned in §1.1, properties may declare context parameters. Those context parameters should be understood of the property "as a whole". + +```kotlin +context(users: UserRepository) +val firstUser: User? get() = users.getById(1) +``` + +Properties declared with context parameters have [**no** _backing field_](https://kotlinlang.org/spec/declarations.html#getters-and-setters). In turn, that means that they may **not** declare an _initializer_. The underlying reason is that the value for the context parameter is not available until the property is accessed, and may change according to the context. ```kotlin // not allowed (property with initializer) context(users: UserRepository) val firstUser: User? = users.getById(1) +``` -// allowed -context(users: UserRepository) -val firstUser: User? get() = users.getById(1) +It is **not** possible to declare context parameters solely for the getter or setter. It is unclear how a property reference (`::firstUser`) should behave if the set of context parameters differs between the getter and the setter. + +```kotlin +// not allowed (context parameter in getter) +val firstUser: User? + context(users: UserRepository) get() = users.getById(1) ``` -The underlying reason is that the value for the context parameter is not available until the property is accessed, and may change according to the context. +At this point, properties with context parameters may **not** use [_delegation_](https://kotlinlang.org/spec/declarations.html#delegated-property-declaration). It is unclear at this point what should be the correct semantics for such a declaration. **§1.4** *(implicitness)*: When calling a function or property with context parameters, those are not spelled out. Rather, the value for each of those arguments is **resolved** from two sources: in-scope context parameters, and implicit receivers ([as defined by the Kotlin specification](https://kotlinlang.org/spec/overload-resolution.html#receivers)). We say that context parameters are **implicit**. From ba8c4808f6157901135f95ea9f4ca07189f0e3aa Mon Sep 17 00:00:00 2001 From: Alejandro Serrano Date: Thu, 4 Apr 2024 09:44:59 +0200 Subject: [PATCH 25/28] Additional clarification in context resolution --- proposals/context-parameters.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/proposals/context-parameters.md b/proposals/context-parameters.md index e48469356..662f7be23 100644 --- a/proposals/context-parameters.md +++ b/proposals/context-parameters.md @@ -498,6 +498,8 @@ The compiler should perform a lookahead of two additional tokens to disambiguate **§7.4** *(applicability, context resolution)*: After the first phase of function applicability -- checking the type constraint problem -- an additional **context resolution** phase is inserted. For each potentially applicable callable, for each context parameter, we traverse the tower of scopes looking for **exactly one** default receiver or context parameter with a compatible type. +If a type of a declared context parameter of a candidate uses a **generic type** whose value is not determined yet, then the corresponding type constraints are added to the constraint system of this candidate call. If solving this system fails, then the candidate is considered to be inapplicable, without trying to substitute different implicit receivers available in the context. + There are three possible outcomes of this process: 1. If _no_ compatible context value is found for at least one context parameter, then the call is _not_ applicable, and it is removed from the candidate set as a result. From da7ee6c6ac502d0cfe7544fece2f8e0bb960e88a Mon Sep 17 00:00:00 2001 From: Alejandro Serrano Date: Fri, 26 Apr 2024 11:18:21 +0200 Subject: [PATCH 26/28] Suggestions from code review --- proposals/context-parameters.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/proposals/context-parameters.md b/proposals/context-parameters.md index 662f7be23..aceb732bb 100644 --- a/proposals/context-parameters.md +++ b/proposals/context-parameters.md @@ -51,6 +51,7 @@ This document is not (yet) formally a KEEP, since it lacks some of the technical **§1.1** *(declaration)*: Function and property declarations get support for **context parameters**. Context parameters are declared with the `context` keyword followed by a list of parameters, each of the form `name: Type`. * Constructors may **not** declare context parameters. +* Context parameters are allowed in operators, except for those related to [property delegation](https://kotlinlang.org/spec/declarations.html#delegated-property-declaration). * Within the body of the declaration, the value of the context parameter is accessible using its name, similar to value parameters. * It is allowed to use `_` as a name; in that case, the value is not accessible through any name (but still participates in context resolution). @@ -172,6 +173,7 @@ fun context(a: A, b: B, c: C, block: context(A, B, C) () -> R): R = **§2.2** *(`implicit` function)*: We also provide a generic way to obtain a value by type from the context. It allows access to context parameters even when declared using `_`, or within the body of a lambda. * Implementations are encouraged, but not required, to mark this function as `inline`. +* If possible, type inference for type variable `A` should be restricted (for example, using `@NoInfer`). Developers are expected to use the function with explicit type arguments. ```kotlin context(ctx: A) fun implicit(): A = ctx From 425a3d019774788530c3cb652beccda6fb41e505 Mon Sep 17 00:00:00 2001 From: Alejandro Serrano Date: Wed, 12 Jun 2024 14:53:41 +0200 Subject: [PATCH 27/28] Equality of callable references with context parameters --- proposals/context-parameters.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/proposals/context-parameters.md b/proposals/context-parameters.md index aceb732bb..f53443d41 100644 --- a/proposals/context-parameters.md +++ b/proposals/context-parameters.md @@ -424,7 +424,9 @@ context(users: UserService) fun saveAll(users: List): Unit = users.forEach(::save) // ::save is resolved as (User) -> Unit ``` -**§5.3** *(callable references, future)*: We consider as **future** improvement a more complex resolution of callables, in which the context is taken into account when the callable is used as an argument of a function that expects a function type with context. +**§5.3** *(callable references, equality)*: Captured context parameters should be treated in the same way as captured receivers for the purposes of function equality and hashing. + +**§5.4** *(callable references, future)*: We consider as **future** improvement a more complex resolution of callables, in which the context is taken into account when the callable is used as an argument of a function that expects a function type with context. ## Context and classes From 5803ed69782e15ede569c5544841869a52340e3a Mon Sep 17 00:00:00 2001 From: Alejandro Serrano Date: Fri, 19 Jul 2024 09:48:41 +0200 Subject: [PATCH 28/28] Update property examples --- proposals/context-parameters.md | 16 ++++------------ 1 file changed, 4 insertions(+), 12 deletions(-) diff --git a/proposals/context-parameters.md b/proposals/context-parameters.md index f53443d41..011c4b52c 100644 --- a/proposals/context-parameters.md +++ b/proposals/context-parameters.md @@ -9,8 +9,6 @@ This is an updated proposal for [KEEP-259](https://github.com/Kotlin/KEEP/issues/259), formerly known as _context receivers_. The new design addresses the issues raised by the users of the prototype implementing that previous iteration of the design and across the community at large. -This document is not (yet) formally a KEEP, since it lacks some of the technical elements. Those are going to be provided at a later time, but we thought it would be interesting to open the discussion even if the design is not fully formalized. - ### Summary of changes from the [previous proposal](https://github.com/Kotlin/KEEP/issues/259) 1. Introduction of named context parameters, @@ -65,7 +63,7 @@ context(analysisScope: AnalysisScope) fun Type.equalTo(other: Type): Boolean = ... context(_: AnalysisScope) -val Type.isBoolean: Boolean = this.equalTo(BuiltIns.Boolean) +val Type.isBoolean: Boolean get() = this.equalTo(BuiltIns.Boolean) ``` **§1.2** *(restrictions)*: @@ -84,17 +82,11 @@ Properties declared with context parameters have [**no** _backing field_](https: ```kotlin // not allowed (property with initializer) -context(users: UserRepository) -val firstUser: User? = users.getById(1) +context(_: AnalysisScope) +val Type.isBoolean: Boolean = this.equalTo(BuiltIns.Boolean) ``` -It is **not** possible to declare context parameters solely for the getter or setter. It is unclear how a property reference (`::firstUser`) should behave if the set of context parameters differs between the getter and the setter. - -```kotlin -// not allowed (context parameter in getter) -val firstUser: User? - context(users: UserRepository) get() = users.getById(1) -``` +It is **not** possible to declare context parameters solely for the getter or setter. It is unclear how a property reference should behave if the set of context parameters differs between the getter and the setter. At this point, properties with context parameters may **not** use [_delegation_](https://kotlinlang.org/spec/declarations.html#delegated-property-declaration). It is unclear at this point what should be the correct semantics for such a declaration.