|
| 1 | +# Explicit context arguments |
| 2 | + |
| 3 | +* **Type**: Design proposal |
| 4 | +* **Author**: Alejandro Serrano |
| 5 | +* **Contributors**: Marat Akhin, Nikita Bobko, Kirill Rakhman |
| 6 | +* **Discussion**: |
| 7 | +* **Status**: Experimental in 2.3.20 |
| 8 | +* **Related YouTrack issue**: |
| 9 | + |
| 10 | +## Abstract |
| 11 | + |
| 12 | +We propose an extension to calls to functions with |
| 13 | +[context parameters](./KEEP-0367-context-parameters.md), |
| 14 | +so that context arguments can be given explicitly. |
| 15 | + |
| 16 | +## Table of contents |
| 17 | + |
| 18 | +* [Abstract](#abstract) |
| 19 | +* [Table of contents](#table-of-contents) |
| 20 | +* [Motivation](#motivation) |
| 21 | +* [Proposal](#proposal) |
| 22 | +* [Alternative design choices](#alternative-design-choices) |
| 23 | + |
| 24 | +## Motivation |
| 25 | + |
| 26 | +In the original proposal for [context parameters](./KEEP-0367-context-parameters.md) |
| 27 | +the only way to provide the values for context arguments is to do so |
| 28 | +**implicitly**, via the new |
| 29 | +[context resolution](./KEEP-0367-context-parameters.md#extended-resolution-algorithm) |
| 30 | +phase that happens as part of |
| 31 | +[overload resolution](https://kotlinlang.org/spec/overload-resolution.html#description). |
| 32 | +In practical terms, this means that context arguments either comes from |
| 33 | +context parameters declared in the signature, or from a call to `context` |
| 34 | +(or a similar function) that introduces new implicit values in the context. |
| 35 | + |
| 36 | +This schema has proven too rigid in practical scenarios. The first issue is |
| 37 | +that even giving a single context arguments requires some **nesting**, because |
| 38 | +of the call to `context`. |
| 39 | + |
| 40 | +The second issue is the lack of **disambiguation** procedures between different |
| 41 | +overloads of the same function. We remark here that the problem is not per se |
| 42 | +that we mark calls as ambiguous, but rather the developer has no easy recourse. |
| 43 | + |
| 44 | +Take the following example, |
| 45 | + |
| 46 | +```kotlin |
| 47 | +context(theA: A) fun foo() { ... } |
| 48 | +context(theB: B) fun foo() { ... } |
| 49 | + |
| 50 | +context(oneA: A, oneB: B) fun bar() { |
| 51 | + foo() // resolution ambiguity |
| 52 | +} |
| 53 | +``` |
| 54 | + |
| 55 | +The way to solve the ambiguity in that examples is to create an additional |
| 56 | +(useless) function that fixes one of the overloads. |
| 57 | + |
| 58 | +```kotlin |
| 59 | +context(oneA: A) fun fooFromA() { |
| 60 | + foo() // only the first one is applicable |
| 61 | +} |
| 62 | + |
| 63 | +context(oneA: A, oneB: B) fun bar() { |
| 64 | + fooFromA() |
| 65 | +} |
| 66 | +``` |
| 67 | + |
| 68 | +However, such a procedure does not scale, and there are situation in which |
| 69 | +applying it becomes much harder (for example, if there are intermediate values, |
| 70 | +they need to be captured). |
| 71 | + |
| 72 | +## Proposal |
| 73 | + |
| 74 | +We propose to alleviate those problems by introducing **explicit context |
| 75 | +arguments**. In short, we can use named parameter syntax to explicitly bind |
| 76 | +a value to a context parameter. |
| 77 | + |
| 78 | +```kotlin |
| 79 | +context(oneA: A, oneB: B) fun bar() { |
| 80 | + foo(theA = oneA) |
| 81 | +} |
| 82 | +``` |
| 83 | + |
| 84 | +This explicit syntax immediately helps with the nesting issue. Furthermore, |
| 85 | +the name of the context parameter provides an additional source for |
| 86 | +disambiguation. |
| 87 | + |
| 88 | +**Resolution stage.** Explicit context arguments are resolved at the same |
| 89 | +time as other named arguments to the function, _not_ during the context |
| 90 | +resolution phase. This choice has consequences for type inference and overload |
| 91 | +resolution, that may now use additional information from those arguments. |
| 92 | + |
| 93 | +**More specific candidate.** There are no changes to the |
| 94 | +[most specific candidate rule](./KEEP-0367-context-parameters.md#extended-resolution-algorithm) |
| 95 | +with respect to context arguments. In particular, a function with context |
| 96 | +parameters (even if all of them are explicitly given) is considered more |
| 97 | +specific than one without. |
| 98 | + |
| 99 | +For example, in the following example, |
| 100 | + |
| 101 | +```kotlin |
| 102 | +context(a: A) fun foo() { ... } // (1) |
| 103 | +fun foo(a: A) { ... } // (2) |
| 104 | + |
| 105 | +fun test() { |
| 106 | + foo(a = A) // resolves to (1) |
| 107 | +} |
| 108 | +``` |
| 109 | + |
| 110 | +Note that you can still disambiguate between each of them: you can use |
| 111 | +`context` to be explicit about (1), or you can give the arguments positionally |
| 112 | +to choose (2). |
| 113 | + |
| 114 | +**Context parameters naming.** |
| 115 | +Context parameters whose name is declared as `_` may not be given as explicit |
| 116 | +context argument. We strongly recommend to give names to context parameters, |
| 117 | +and make them _unique_ among similar overloads, so the name can be used for |
| 118 | +disambiguation purposes. |
| 119 | + |
| 120 | +**Contextual properties.** This proposal does not introduce syntax for |
| 121 | +explicitly providing context arguments to contextual properties. |
| 122 | + |
| 123 | +```kotlin |
| 124 | +context(a: A) val message: String get() = ... |
| 125 | + |
| 126 | +context(A()) { message } // accepted |
| 127 | +message(a = A()) // unresolved |
| 128 | +``` |
| 129 | + |
| 130 | +We remark that the nesting in this case is required. If we used a property |
| 131 | +reference instead, `context(A(), ::message)`, then the context argument is |
| 132 | +eagerly resolved as explained in the |
| 133 | +[callable references](./KEEP-0367-context-parameters.md#callable-references) |
| 134 | +section of the original proposal. |
| 135 | + |
| 136 | +**DSL marker.** Explicit context arguments are _not_ affected by the |
| 137 | +applicability restrictions of the `DslMarker` annotation. This aligns with |
| 138 | +the behavior for receivers, which also may overcome the restrictions by |
| 139 | +explicitly giving them. |
| 140 | + |
| 141 | +**Interaction with [named-only parameters](https://github.com/Kotlin/KEEP/blob/main/proposals/KEEP-0439-named-only-parameters.md).** |
| 142 | +Although still in a proposal stage, Kotlin may introduce requirements for |
| 143 | +using a parameter only in named form. |
| 144 | + |
| 145 | +```kotlin |
| 146 | +fun foo(named a: A) { ... } |
| 147 | +``` |
| 148 | + |
| 149 | +Alas, this means that the usual disambiguation technique (using positional |
| 150 | +arguments) may no longer be used here. There's no proposed solution at this |
| 151 | +point, other than recommending library authors to not provide both versions. |
| 152 | + |
| 153 | +## Alternative design choices |
| 154 | + |
| 155 | +Some alternative choices were considered during the design. |
| 156 | + |
| 157 | +**All-or-nothing.** The current proposal allows to given any amount of |
| 158 | +explicit context arguments, with the rest of them being resolved in the |
| 159 | +usual way. An alternative is to only allow this syntax when all of the |
| 160 | +context arguments are given explicitly. |
| 161 | + |
| 162 | +This model, however, makes it difficult to introduce new context parameters |
| 163 | +into existing functions. What should be a simple additional `context` wrapper |
| 164 | +or context parameter in the signature ends up requiring changes in every |
| 165 | +place in which the context arguments are given explicitly. |
| 166 | + |
| 167 | +Being more flexible also means the proposal covers an additional use case, |
| 168 | +namely _slightly adjusting_ the context for a function, but without requiring |
| 169 | +explicitly repeating all the others. |
| 170 | + |
| 171 | +```kotlin |
| 172 | +context(logger: Logger, service: ThingService) |
| 173 | +fun doSomething() { ... } |
| 174 | + |
| 175 | +context(logger: Logger, service: ThingService) |
| 176 | +fun doSomethingBigger() { |
| 177 | + doSomething(logger = OnlyCritical(logger)) |
| 178 | + // instead of requiring |
| 179 | + doSomething(logger = OnlyCritical(logger), service = service) |
| 180 | +} |
| 181 | +``` |
| 182 | + |
| 183 | +Finally, we think that a call in which only some context arguments are given |
| 184 | +explicitly is not more difficult to understand than one in which all of them |
| 185 | +are explicitly given. The intuitive model (those not explicitly given are |
| 186 | +implicitly resolved) is still simple to understand. |
| 187 | + |
| 188 | +**More specific candidate.** We have considered a variation of the rules |
| 189 | +for _more specific candidate_ in which a function call in which all context |
| 190 | +arguments are explicitly given would count as having no contexts. |
| 191 | + |
| 192 | +```kotlin |
| 193 | +context(a: A) fun foo() { ... } // (1) |
| 194 | +fun foo(a: A) { ... } // (2) |
| 195 | + |
| 196 | +fun test() { |
| 197 | + foo(a = A) // ambiguity |
| 198 | +} |
| 199 | +``` |
| 200 | + |
| 201 | +However, the benefits of such variation are unclear, whereas it introduces |
| 202 | +even more complication to a somehow convoluted rule. |
0 commit comments