|
| 1 | +# Named-only parameters |
| 2 | + |
| 3 | +* **Type**: Design proposal |
| 4 | +* **Author**: Roman Efremov |
| 5 | +* **Contributors**: Alejandro Serrano Mena, Denis Zharkov, Dmitriy Novozhilov, Faiz Ilham Muhammad, Marat Akhin, Mikhail Zarečenskij, Nikita Bobko, Pavel Kunyavskiy, Roman Venediktov |
| 6 | +* **Discussion**: [#442](https://github.com/Kotlin/KEEP/discussions/442) |
| 7 | +* **Status**: KEEP discussion |
| 8 | +* **Related YouTrack issue**: [KT-14934](https://youtrack.jetbrains.com/issue/KT-14934) |
| 9 | + |
| 10 | +## Abstract |
| 11 | + |
| 12 | +We introduce a new modifier `named` on function value parameters that obliges callers of this function to specify the name of this parameter. |
| 13 | +This enhances code readability and reduces errors caused by value associated with the wrong parameter when passed in positional form. |
| 14 | + |
| 15 | +Here's an example: |
| 16 | + |
| 17 | +```kotlin |
| 18 | +fun String.reformat(named normalizeCase: Boolean, named upperCaseFirstLetter: Boolean): String { /* body */ } |
| 19 | + |
| 20 | +str.reformat(false, true) // ❌ ERR |
| 21 | +str.reformat(normalizeCase = false, upperCaseFirstLetter = true) // ✅ OK |
| 22 | +``` |
| 23 | + |
| 24 | + |
| 25 | +## Table of contents |
| 26 | + |
| 27 | +- [Motivation](#motivation) |
| 28 | + - [Examples](#examples) |
| 29 | + - [Multiple lambda arguments](#multiple-lambda-arguments) |
| 30 | + - [Need in language-level support](#need-in-language-level-support) |
| 31 | +- [Design](#design) |
| 32 | + - [Basics](#basics) |
| 33 | + - [Overload resolution](#overload-resolution) |
| 34 | + - [Method overrides](#method-overrides) |
| 35 | + - [Expect/actual matching](#expectactual-matching) |
| 36 | + - [Interoperability](#interoperability) |
| 37 | + - [Migration cycle](#migration-cycle) |
| 38 | + - [Compilation](#compilation) |
| 39 | + - [Data class copy() migration](#data-class-copy-migration) |
| 40 | + - [Tooling support](#tooling-support) |
| 41 | +- [Alternatives](#alternatives) |
| 42 | + - ["Fence" approach](#fence-approach) |
| 43 | + - [Annotation or modifier?](#annotation-or-modifier) |
| 44 | + |
| 45 | +## Motivation |
| 46 | + |
| 47 | +There are two usual ways of [passing arguments to functions](https://kotlinlang.org/docs/functions.html#function-usage) in Kotlin: positionally or by name (there are more ways, like default arguments or infix functions, but we're not looking at them in this paragraph). |
| 48 | + |
| 49 | +Sometimes passing an argument in positional form is not desirable because of the resulting ambiguity. |
| 50 | +For example, consider the following function: |
| 51 | + |
| 52 | +```kotlin |
| 53 | +fun reformat( |
| 54 | + str: String, |
| 55 | + normalizeCase: Boolean = true, |
| 56 | + upperCaseFirstLetter: Boolean = true, |
| 57 | + divideByCamelHumps: Boolean = false, |
| 58 | + wordSeparator: Char = ' ', |
| 59 | +) { /*...*/ } |
| 60 | +``` |
| 61 | + |
| 62 | +When it is called without argument names, for example `reformat(myStr, true, false, true, ' ')`, it's difficult to say for the reader what the meaning of its arguments is. |
| 63 | + |
| 64 | +That's why developers always try to remember to specify the argument names in such cases. |
| 65 | +[Kotlin coding conventions](https://kotlinlang.org/docs/coding-conventions.html#named-arguments) also recommend to use named arguments named argument syntax when a method takes multiple parameters of the same primitive type, or for parameters of `Boolean` type. |
| 66 | + |
| 67 | +In Java, it's also a common practice to specify argument names in the comments, e.g. `reformat(myStr, /* normalizeCase = */ true)`. |
| 68 | +This is a clear sign that argument names can sometimes play a critical role in code readability. |
| 69 | + |
| 70 | +### Examples |
| 71 | + |
| 72 | +Here are several examples of functions that have one or several arguments meant to be passed only in a named form: |
| 73 | + |
| 74 | +| Function | Named-only argument(s) | |
| 75 | +|---------------------------------------------------------------------------------------------------------------------------------|------------------------------------------------------------------------------------------------------------| |
| 76 | +| `fun parentsOf(element: PsiElement, withSelf: Boolean)` | `withSelf` | |
| 77 | +| `fun CharSequence.startsWith(char: Char, ignoreCase: Boolean = false)` | `ignoreCase` | |
| 78 | +| `fun <T> assertEquals(expected: T, actual: T)` | `expected`, optionally `actual` | |
| 79 | +| `fun <T> copy(src: Array<T>, dst: Array<T>)` | `src`, optionally `dst` | |
| 80 | +| `fun <T> Array<out T>.joinToString(separator: CharSequence = ", ", prefix: CharSequence = "", postfix: CharSequence = "")` | all | |
| 81 | +| `fun Modifier.padding(start: Dp = 0.dp, top: Dp = 0.dp, end: Dp = 0.dp, bottom: Dp = 0.dp)` | all | |
| 82 | +| `class RetrySettings(val maxRetries: Int, val delayMillis: Long, val exponentialBackoff: Boolean, val retryOnTimeout: Boolean)` | all | |
| 83 | +| `class User(val userId: Long, val nickname: String, val metaInfo: UserMeta?)` | `metaInfo` – type is pretty self-descriptive, however, name still might be desirable when `null` is passed | |
| 84 | +| `copy()` functions of data classes | all, because usually only a few properties are changed when copying | |
| 85 | + |
| 86 | +If there was a way to enforce a named form of arguments, it would increase the readability for calls of such functions and eliminate mistakes caused by ambiguity in arguments passed positionally. |
| 87 | + |
| 88 | +### Multiple lambda arguments |
| 89 | + |
| 90 | +When the last function argument is of a functional type, it's also possible to use a special [trailing lambda](https://kotlinlang.org/docs/lambdas.html#passing-trailing-lambdas) syntax, in which a corresponding argument can be placed outside the parentheses. |
| 91 | + |
| 92 | +When a function is called with multiple lambda arguments, the correspondence of arguments to parameters can also be ambiguous. |
| 93 | +The most notable examples are: |
| 94 | + |
| 95 | +* `fun Result.process(onSuccess: () → Unit, onError: () → Unit)` |
| 96 | +* `fun <T, K, V> Array<out T>.groupBy(keySelector: (T) -> K, valueTransform: (T) -> V)` |
| 97 | +* `fun <C> Either<C>.fold(ifLeft: (left: A) -> C, ifRight: (right: B) -> C): C` |
| 98 | + |
| 99 | +For these functions, we'd like to restrict a positional form of arguments, e.g. `result.process({}, {})`. |
| 100 | +In addition to that, we'd like to restrict the last lambda argument to be passed in trailing form, e.g. `result.process(onSuccess = {}) { println("or error") }`. |
| 101 | + |
| 102 | +### Need in language-level support |
| 103 | + |
| 104 | +It's possible to enforce the named form of function arguments by static code analysis tools ([example](https://detekt.dev/docs/next/rules/potential-bugs/#unnamedparameteruse)), by an IDE inspection ([example](https://www.jetbrains.com/help/inspectopedia/BooleanLiteralArgument.html)) or by implementing a compiler plugin. |
| 105 | + |
| 106 | +However, such tools don't solve the problem completely because they require preliminary setup. |
| 107 | +Users must adjust their build setup themselves to make it work. |
| 108 | +This is not a practical approach in the case of libraries that would like to enforce this rule on consumers of their APIs. |
| 109 | +Thus, this proposal aims to provide language-level support for this feature. |
| 110 | + |
| 111 | +## Design |
| 112 | + |
| 113 | +### Basics |
| 114 | + |
| 115 | +It’s proposed to introduce a notion of named-only value parameters, which is indicated by a new soft keyword modifier `named` put on them. |
| 116 | +When this modifier is applied, the compiler ensures that the argument is passed either with name or omitted (in the case of an argument with a default value). |
| 117 | +Otherwise, an error is produced. |
| 118 | + |
| 119 | +Here's an example to illustrate the behavior: |
| 120 | + |
| 121 | +```kotlin |
| 122 | +fun CharSequence.startsWith(char: Char, named ignoreCase: Boolean = false): Boolean { /* ... */ } |
| 123 | + |
| 124 | +cs.startsWith('a') // OK |
| 125 | +cs.startsWith('a', ignoreCase = false) // OK |
| 126 | +cs.startsWith('a', false) // ERROR |
| 127 | +``` |
| 128 | + |
| 129 | +In the case of lambda parameters, this means that they can't be passed in a trailing form when `named` modifier is applied. |
| 130 | + |
| 131 | +Modifier `named` is applicable for class constructor value parameters. |
| 132 | + |
| 133 | +`named` modifier is not applicable to: |
| 134 | + |
| 135 | +* context parameters |
| 136 | +* parameter of property setter |
| 137 | +* parameters of function types |
| 138 | + |
| 139 | +### Overload resolution |
| 140 | + |
| 141 | +The presence of the `named` modifier doesn't affect overload resolution. |
| 142 | +The check of whether a mandatory name of an argument is provided happens after resolution. |
| 143 | +Here's an example that demonstrates this: |
| 144 | + |
| 145 | +```kotlin |
| 146 | +fun foo(named p: Int) {} // (1) |
| 147 | +fun foo(p: Any) {} // (2) |
| 148 | + |
| 149 | +fun main() { |
| 150 | + foo(1) // resolved to (1) and reports an error about a missing argument name |
| 151 | + // despite that (2) is an applicable candidate |
| 152 | +} |
| 153 | +``` |
| 154 | + |
| 155 | +Why not do the opposite – affect overload resolution? One of the reasons is that it would lead to unwanted resolution change. |
| 156 | + |
| 157 | +Consider the following example: |
| 158 | + |
| 159 | +```kotlin |
| 160 | +class MyOptimizedString : CharSequence { /* ... */ } |
| 161 | + |
| 162 | +fun MyOptimizedString.indexOf(string: String, startIndex: Int = 0, ignoreCase: Boolean = false): Int = TODO() |
| 163 | + |
| 164 | +fun test(s: CharSequence, substr: String) { |
| 165 | + if (s is MyOptimizedString) { |
| 166 | + val i = s.indexOf(substr, 0, true) |
| 167 | + } |
| 168 | +} |
| 169 | +``` |
| 170 | + |
| 171 | +Here, we define a class `MyOptimizedString` optimized for substring search operations. |
| 172 | +We also define a specific override of a `indexOf` function with a signature similar to standard `CharSequence#indexOf`. |
| 173 | + |
| 174 | +Suppose that we decided to adopt a new feature and put `named` modifier on the parameters `startIndex` and `ignoreCase` of our function. |
| 175 | +What we expect to happen after a change is that all places in the code where names of respective arguments are not specified will be reported as errors. |
| 176 | +What will happen instead, however, is that all incorrect `indexOf` calls, like the one in `test` function, will silently change their resolve to more general overload `CharSequence#indexOf` because its parameters don't require names. |
| 177 | + |
| 178 | +Another drawback when `named` parameters affect resolution is that skipping argument becomes a way to resolve overload ambiguity. |
| 179 | +But it's absurd to assume that an argument name is omitted not because of a mistake, but because a code writer intentionally wants to choose a specific overload. |
| 180 | + |
| 181 | +### Method overrides |
| 182 | + |
| 183 | +It's a warning when the presence of `named` modifier is different on overriding and overridden method. |
| 184 | + |
| 185 | +Whenever a callable with the same signature is inherited from more than one superclass (sometimes known as an _intersection override_), |
| 186 | +the parameter in intersection gets a `named` modifier if all the respective parameters from overridden functions are `named` **and** have the same name. |
| 187 | + |
| 188 | +```kotlin |
| 189 | +interface A { |
| 190 | + fun foo(named p: Int) |
| 191 | + fun bar(named p: Int) |
| 192 | + fun baz(named p: Int) |
| 193 | +} |
| 194 | + |
| 195 | +interface B { |
| 196 | + fun foo(named p: Int) |
| 197 | + fun bar(named other: Int) |
| 198 | + fun baz(p: Int) |
| 199 | +} |
| 200 | + |
| 201 | +interface C : A, B { |
| 202 | + // intersection overrides |
| 203 | + // fun foo(named p: Int) |
| 204 | + // fun bar(<ambiguous>: Int) |
| 205 | + // fun baz(p: Int) |
| 206 | +} |
| 207 | +``` |
| 208 | + |
| 209 | +### Expect/actual matching |
| 210 | + |
| 211 | +Presence of `named` modifier on parameter must strictly match on the `expect` and `actual` declarations, otherwise it's an error. |
| 212 | +For `actual` declarations coming from Java this means that matching is not possible, when `expect` declaration has `named` parameter. |
| 213 | + |
| 214 | +The main motivation for this is the issues with the current Kotlin Multiplatform compilation model. |
| 215 | +The problem is, in certain scenarios common code can see platform declarations ([KT-66205](https://youtrack.jetbrains.com/issue/KT-66205)). |
| 216 | +If we allowed a Kotlin `expect` function with `named` parameter to actualize to Java declaration, such a function can't even be called from the common code of a dependent project. |
| 217 | +This is because it will see named-only parameter in one compilation, and Java (effectively positional-only) parameter in another, which contradict each other. |
| 218 | +Thus, it was decided to start with the most restrictive rules to have less confusing behavior. |
| 219 | + |
| 220 | +### Interoperability |
| 221 | + |
| 222 | +Presence of `named` on the arguments doesn't affect the way Kotlin functions are exported and called in other languages. |
| 223 | +For example, in case of Java, `named` arguments of Kotlin function will be passed in positional form. |
| 224 | + |
| 225 | +### Migration cycle |
| 226 | + |
| 227 | +Library authors might want to integrate this feature into already existing APIs. |
| 228 | +To make the migration process smooth and give authors full control of when warnings will be turned into errors, it's proposed to introduce a new annotation in Kotlin standard library: |
| 229 | + |
| 230 | +```kotlin |
| 231 | +@Target(FUNCTION, CONSTRUCTOR, FILE) |
| 232 | +@Retention(BINARY) |
| 233 | +annotation class SoftNamedOnlyParametersCheck(named val ideOnlyDiagnostic: Boolean = false) |
| 234 | +``` |
| 235 | + |
| 236 | +When this annotation is set on a function or constructor, it will soften all errors caused by the missing name of the argument to warnings. |
| 237 | +Here's an example: |
| 238 | + |
| 239 | +```kotlin |
| 240 | +@SoftNamedOnlyParameterCheck |
| 241 | +fun CharSequence.startsWith(char: Char, named ignoreCase: Boolean = false): Boolean { /* ... */ } |
| 242 | + |
| 243 | +cs.startsWith('a', false) // WARNING |
| 244 | +``` |
| 245 | + |
| 246 | +When the annotation is applied to a data class constructor, it's applied to a generated `copy()` function as well. |
| 247 | +When the annotation is applied to a file, it applies to all top-level functions in this file. |
| 248 | + |
| 249 | +> [!NOTE] |
| 250 | +> `VALUE_PARAMETER` target is not in the list of annotation targets, as it's hard to imagine a case when one parameter of a function must be a warning while another one is error. |
| 251 | +
|
| 252 | +In the case of particularly popular libraries (including the Kotlin standard library and kotlinx.* libraries), there easily could be hundreds of calls only within one project. |
| 253 | +Even this solution may seem too harsh, as it may clutter the compiler build log with warnings. |
| 254 | +That's why we introduce an additional `ideOnlyDiagnostic` parameter, which leaves warnings only in IDE and disables the diagnostic everywhere else. |
| 255 | + |
| 256 | +### Compilation |
| 257 | + |
| 258 | +The fact of presence of `named` modifier is saved in the metadata. |
| 259 | +Other than that, the modifier doesn't affect the output artifact of the compiler. |
| 260 | + |
| 261 | +### Reflection |
| 262 | + |
| 263 | +Interface `kotlin.reflect.KParameter` should be extended with a new property `isNamedOnly: Boolean`. |
| 264 | + |
| 265 | +### Data class copy() migration |
| 266 | + |
| 267 | +As previously mentioned in "Examples" section, `copy()` function of data classes is another place where all the parameters are meant to be named-only. |
| 268 | +This observation is also reflected in the [IntelliJ IDEA inspection](https://www.jetbrains.com/help/inspectopedia/CopyWithoutNamedArguments.html) that reports soft warnings in case `copy()` function is called without named arguments. |
| 269 | + |
| 270 | +It's proposed to make all the parameters of `copy()` functions of data classes named-only. |
| 271 | +However, as this is a breaking change, it's proposed to split the migration into two phases: |
| 272 | + |
| 273 | +**Phase 1** |
| 274 | + |
| 275 | +* When compiling data classes from source files, mark all the parameters of `copy()` functions with `named` modifier. |
| 276 | +* When reading data classes produced by older versions of the compiler, treat them as if `named` modifier were set. |
| 277 | +* Soften errors caused by missing names in `copy()` calls to warnings. |
| 278 | +* Provide a compiler flag to fast-forward to Phase 2 (e.g. `-Xnamed-only-parameters-in-data-class-copy`). |
| 279 | + |
| 280 | +**Phase 2** |
| 281 | + |
| 282 | +* Don't soften errors caused by the missing name in `copy()` calls anymore (unless `@SoftNamedOnlyParameterCheck` annotation is applied to data class constructor) |
| 283 | + |
| 284 | +> [!NOTE] The phases are not tied to specific compiler versions or timeline. |
| 285 | +
|
| 286 | +### Tooling support |
| 287 | + |
| 288 | +IDE should suggest a quick fix, which would specify parameter names explicitly when the diagnostic about missing name is reported. |
| 289 | + |
| 290 | +## Alternatives |
| 291 | + |
| 292 | +### "Fence" approach |
| 293 | + |
| 294 | +In current proposal `named` modifier is set on each parameter separately. |
| 295 | +Another option is to apply what can be called "fence" approach where named-only parameters are fenced off from the rest of the parameters by a special symbol or another syntactic element. |
| 296 | +This approach can be seen in languages like Python or Julia. |
| 297 | +In the case of Kotlin, here's what it could look like: |
| 298 | + |
| 299 | +```kotlin |
| 300 | +// All arguments after asterisk are named-only |
| 301 | +fun CharSequence.startsWith(char: Char, *, ignoreCase: Boolean = false) |
| 302 | +fun Modifier.padding(*, start: Dp = 0.dp, top: Dp = 0.dp, |
| 303 | + end: Dp = 0.dp, bottom: Dp = 0.dp) |
| 304 | +``` |
| 305 | + |
| 306 | +This is especially beneficial for functions with a big number of parameters (consider, for example, [TextStyle](https://developer.android.com/reference/kotlin/androidx/compose/ui/text/TextStyle) from Jetpack Compose with 25 parameters). |
| 307 | + |
| 308 | +However, we decided not to go this way. |
| 309 | +The problem is that this approach is based on the assumption that named-only parameters should always go together and reside at the end of the argument list. |
| 310 | +In the meantime, `named` modifier provides more flexibility and supports some important use cases, that do not meet this assumption, namely: |
| 311 | + |
| 312 | +1. In DSLs it's often the case that a group of named-only parameters is followed up by a trailing lambda parameter. |
| 313 | +2. Sometimes the name of the first argument could serve as a continuation of the function name, where right now it leaks into a name of the function. |
| 314 | + |
| 315 | +As an example for a second case, let's take a look at transformation functions from `kotlin.collections`. |
| 316 | +Many of them are available in two variants with names `<transformationname>` and `<transformationname>To`. |
| 317 | +Consider these `fold` and `foldTo` functions: |
| 318 | + |
| 319 | +```kotlin |
| 320 | +fun <T, K, R> Grouping<T, K>.fold( |
| 321 | + initialValue: R, |
| 322 | + operation: (accumulator: R, element: T) -> R |
| 323 | +): Map<K, R> |
| 324 | + |
| 325 | +fun <T, K, R, M : MutableMap<in K, R>> Grouping<T, K>.foldTo( |
| 326 | + destination: M, |
| 327 | + initialValue: R, |
| 328 | + operation: (accumulator: R, element: T) -> R |
| 329 | +): M |
| 330 | +``` |
| 331 | + |
| 332 | +With new feature we could rewrite `foldTo` as an override of `fold` with first parameter being named-only in the following way: |
| 333 | + |
| 334 | +```kotlin |
| 335 | +fun <T, K, R, M : MutableMap<in K, R>> Grouping<T, K>.fold( |
| 336 | + named to: M, |
| 337 | + initialValue: R, |
| 338 | + operation: (accumulator: R, element: T) -> R |
| 339 | +): M |
| 340 | +``` |
| 341 | + |
| 342 | +This way, we reduce the number of function variants while maintaining clarity because the parameter name will always be present. |
| 343 | + |
| 344 | +It's worth stipulating that this doesn't mean we're promoting this style as the better replacement for the previous one. |
| 345 | +It's still up to API authors' preferences. |
| 346 | +The only thing that is important in this example is just that the chosen syntax approach with `named` modifier allows writing in both styles. |
| 347 | + |
| 348 | +### Annotation or modifier? |
| 349 | + |
| 350 | +It's a common question for many language features whether a new modifier should instead be a new annotation. |
| 351 | +In the case of named-only parameters, the choice was made in favor of modifier for the following reasons: |
| 352 | + |
| 353 | +* It looks cleaner in code |
| 354 | +* Easily distinguishable from user-defined annotations |
| 355 | +* The feature aims to be widely used and is not tied to a specific domain or use case (unlike, for example, `DslMarker` or `BuilderInference`) |
| 356 | + |
| 357 | +### Make named-only parameters a new default |
| 358 | + |
| 359 | +We can't go past a possibility to make all parameters named-only by default, which is the way it works in Swift. |
| 360 | +Among named-only, positional-only and flexible naming parameters, the latter option seems to be the most commonly desired. |
| 361 | +That's why we believe the current default is good, and it's not our goal to migrate a whole world. |
| 362 | +Moreover, migration for such a core functionality would be quite onerous. |
0 commit comments