Skip to content

Commit 83bcfe8

Browse files
committed
Add proposal for "Named-only parameters" feature
1 parent 4521d2c commit 83bcfe8

File tree

1 file changed

+362
-0
lines changed

1 file changed

+362
-0
lines changed
Lines changed: 362 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,362 @@
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

Comments
 (0)