|
| 1 | +# Guards |
| 2 | + |
| 3 | +* **Type**: Design proposal |
| 4 | +* **Author**: Alejandro Serrano |
| 5 | +* **Contributors**: Mikhail Zarechenskii |
| 6 | +* **Discussion**: [KEEP-371](https://github.com/Kotlin/KEEP/issues/371) |
| 7 | +* **Status**: Experimental expected for 2.1 |
| 8 | +* **Related YouTrack issue**: [KT-13626](https://youtrack.jetbrains.com/issue/KT-13626) |
| 9 | + |
| 10 | +## Abstract |
| 11 | + |
| 12 | +We propose an extension of branches in `when` expressions with subject, which unlock the ability to perform multiple checks in the condition of the branch. |
| 13 | + |
| 14 | +## Table of contents |
| 15 | + |
| 16 | +* [Abstract](#abstract) |
| 17 | +* [Table of contents](#table-of-contents) |
| 18 | +* [Motivating example](#motivating-example) |
| 19 | + * [Exhaustiveness](#exhaustiveness) |
| 20 | + * [Clarity over power](#clarity-over-power) |
| 21 | + * [One little step](#one-little-step) |
| 22 | +* [Technical details](#technical-details) |
| 23 | + * [Style guide](#style-guide) |
| 24 | + * [The need for `else`](#the-need-for-else) |
| 25 | + * [Alternative syntax using `&&`](#alternative-syntax-using-) |
| 26 | +* [Potential extensions](#potential-extensions) |
| 27 | + * [De-duplication of heads](#de-duplication-of-heads) |
| 28 | + * [Abbreviated access to subject members](#abbreviated-access-to-subject-members) |
| 29 | + |
| 30 | +## Motivating example |
| 31 | + |
| 32 | +Consider the following types, which model the state of a UI application. |
| 33 | + |
| 34 | +```kotlin |
| 35 | +enum class Problem { |
| 36 | + CONNECTION, AUTHENTICATION, UNKNOWN |
| 37 | +} |
| 38 | + |
| 39 | +sealed interface Status { |
| 40 | + data object Loading: Status |
| 41 | + data class Error(val problem: Problem, val isCritical: Boolean): Status |
| 42 | + data class Ok(val info: List<String>): Status |
| 43 | +} |
| 44 | +``` |
| 45 | + |
| 46 | +Kotlin developers routinely use [`when` expressions](https://kotlinlang.org/docs/control-flow.html#when-expression) |
| 47 | +with subject to describe the control flow of the program. |
| 48 | + |
| 49 | +```kotlin |
| 50 | +fun render(status: Status): String = when (status) { |
| 51 | + Status.Loading -> "loading" |
| 52 | + is Status.Error -> "error: ${status.problem}" |
| 53 | + is Status.Ok -> status.info.joinToString() |
| 54 | +} |
| 55 | +``` |
| 56 | + |
| 57 | +Note how [smart casts](https://kotlinlang.org/docs/typecasts.html#smart-casts) interact with |
| 58 | +`when` expressions. In each branch we get access to those fields available only on the |
| 59 | +corresponding subclass of `Status`. |
| 60 | + |
| 61 | +Using a `when` expression with subject has one important limitation, though: each branch must |
| 62 | +depend on a _single condition over the subject_. _Guards_ remove this limitation, so you |
| 63 | +can introduce additional conditions, separated with `if` from the subject condition. |
| 64 | + |
| 65 | +```kotlin |
| 66 | +fun render(status: Status): String = when (status) { |
| 67 | + Status.Loading -> "loading" |
| 68 | + is Status.Ok if status.info.isEmpty() -> "no data" |
| 69 | + is Status.Ok -> status.info.joinToString() |
| 70 | + is Status.Error if status.problem == Problem.CONNECTION -> |
| 71 | + "problems with connection" |
| 72 | + is Status.Error if status.problem == Problem.AUTHENTICATION -> |
| 73 | + "could not be authenticated" |
| 74 | + else -> "unknown problem" |
| 75 | +} |
| 76 | +``` |
| 77 | + |
| 78 | +The combination of guards and smart casts gives us the ability to look "more than one layer deep". |
| 79 | +For example, after we know that `status` is `Status.Error`, we get access to the `problem` field. |
| 80 | + |
| 81 | +The code above can be rewritten using nested control flow, but "flattening the code" is not the |
| 82 | +only advantage of guards. They can also simplify complex control logic. For example, the code |
| 83 | +below `render`s both non-critical problems and success with an empty list in the same way. |
| 84 | + |
| 85 | +```kotlin |
| 86 | +fun render(status: Status): String = when (status) { |
| 87 | + Status.Loading -> "loading" |
| 88 | + is Status.Ok if status.info.isNotEmpty() -> status.info.joinToString() |
| 89 | + is Status.Error if status.isCritical -> "critical problem" |
| 90 | + else -> "problem, try again" |
| 91 | +} |
| 92 | +``` |
| 93 | + |
| 94 | +If we rewrite the code above using a subject and nested control flow, we need to |
| 95 | +_repeat_ some code. This hurts maintainability since we need to remember to |
| 96 | +keep the two `"problem, try again"` in sync. |
| 97 | + |
| 98 | +```kotlin |
| 99 | +fun render(status: Status): String = when (status) { |
| 100 | + Status.Loading -> "loading" |
| 101 | + is Status.Ok -> |
| 102 | + if (status.info.isNotEmpty()) status.info.joinToString() |
| 103 | + else "problem, try again" |
| 104 | + is Status.Error -> |
| 105 | + if (status.isCritical) "critical problem" |
| 106 | + else "problem, try again" |
| 107 | +} |
| 108 | +``` |
| 109 | + |
| 110 | +It is possible to switch to a `when` expression without subject, |
| 111 | +but that obscures the fact that our control flow depends |
| 112 | +on the `Status` value. |
| 113 | + |
| 114 | +```kotlin |
| 115 | +fun render(status: Status): String = when { |
| 116 | + status == Status.Loading -> "loading" |
| 117 | + status is Status.Ok && status.info.isNotEmpty() -> status.info.joinToString() |
| 118 | + status is Status.Error && status.isCritical -> "critical problem" |
| 119 | + else -> "problem, try again" |
| 120 | +} |
| 121 | +``` |
| 122 | + |
| 123 | +The Kotlin compiler provides more examples of this pattern, like |
| 124 | +[this one](https://github.com/JetBrains/kotlin/blob/112473f310b9491e79592d4ba3586e6f5da06d6a/compiler/fir/resolve/src/org/jetbrains/kotlin/fir/resolve/calls/Candidate.kt#L188) |
| 125 | +during candidate resolution. |
| 126 | + |
| 127 | +### Exhaustiveness |
| 128 | + |
| 129 | +Given that guards may include any expression, branches including a guard should |
| 130 | +be ignored for the purposes of [exhaustiveness](https://kotlinlang.org/spec/expressions.html#exhaustive-when-expressions). |
| 131 | +If you need to ensure that you have covered every case when nested properties are |
| 132 | +involved, we recommend using nested conditionals instead. |
| 133 | + |
| 134 | +```kotlin |
| 135 | +// don't do this |
| 136 | +when (status) { |
| 137 | + is Status.Error if status.problem == Problem.CONNECTION -> ... |
| 138 | + is Status.Error if status.problem == Problem.AUTHENTICATION -> ... |
| 139 | + is Status.Error if status.problem == Problem.UNKNOWN -> ... |
| 140 | + // the compiler requires an additional 'is Status.Error' case |
| 141 | +} |
| 142 | + |
| 143 | +// better do this |
| 144 | +when (status) { |
| 145 | + is Status.Error -> when (status.problem) { |
| 146 | + Problem.CONNECTION -> ... |
| 147 | + Problem.AUTHENTICATION -> ... |
| 148 | + Problem.UNKNOWN -> ... |
| 149 | + } |
| 150 | +} |
| 151 | +``` |
| 152 | + |
| 153 | +### Clarity over power |
| 154 | + |
| 155 | +One important design goal of this proposal is to make code _clear_ to write and read. |
| 156 | +For that reason, we have consciously removed some of the potential corner cases. |
| 157 | +In addition, we have conducted a survey to ensure that guards are intuitively understood |
| 158 | +by a wide range of Kotlin developers. |
| 159 | + |
| 160 | +On that line, it is not allowed to mix several conditions (using `,`) and guards in a single branch. |
| 161 | +They can be used in the same `when` expression, though. |
| 162 | +Here is one example taken from [JEP-441](https://openjdk.org/jeps/441). |
| 163 | + |
| 164 | +```kotlin |
| 165 | +fun testString(response: String?) { |
| 166 | + when (response) { |
| 167 | + null -> { } |
| 168 | + "y", "Y" -> println("You got it") |
| 169 | + "n", "N" -> println("Shame") |
| 170 | + else if response.equals("YES", ignoreCase = true) -> |
| 171 | + println("You got it") |
| 172 | + else if response.equals("NO", ignoreCase = true) -> |
| 173 | + println("Shame") |
| 174 | + else -> println("Sorry") |
| 175 | + } |
| 176 | +} |
| 177 | +``` |
| 178 | + |
| 179 | +This example also shows that the combination of `else` with a guard |
| 180 | +-- in other words, a branch that does not inspect the subject -- |
| 181 | +is done with special `else if` syntax. |
| 182 | + |
| 183 | +The reason behind this restriction is that the following condition |
| 184 | + |
| 185 | +```kotlin |
| 186 | +condition1, condition2 if guard |
| 187 | +``` |
| 188 | + |
| 189 | +is not immediately unambiguous. Is the `guard` checked only when |
| 190 | +`condition2` matches, or is it checked in every case? |
| 191 | + |
| 192 | +### One little step |
| 193 | + |
| 194 | +Whereas other languages have introduced full pattern matching |
| 195 | +(like Java in [JEP-440](https://openjdk.org/jeps/440) and [JEP-441](https://openjdk.org/jeps/441)), |
| 196 | +we find guards to be a much smaller extension to the language, |
| 197 | +but covering many of those use cases. This is possible because |
| 198 | +we benefit from the powerful data- and control-flow analysis in |
| 199 | +the language. If we perform a type test on the subject, then |
| 200 | +we can access all the fields corresponding to that type in the guard. |
| 201 | + |
| 202 | +## Technical details |
| 203 | + |
| 204 | +We extend the syntax of `whenEntry` in the following way. |
| 205 | + |
| 206 | +``` |
| 207 | +whenEntry : whenCondition [{NL} whenEntryAddition] {NL} '->' {NL} controlStructureBody [semi] |
| 208 | + | 'else' [ whenEntryGuard ] {NL} '->' {NL} controlStructureBody [semi] |
| 209 | +
|
| 210 | +whenEntryAddition: ',' [ {NL} whenCondition { {NL} ',' {NL} whenCondition} [ {NL} ',' ] ] |
| 211 | + | whenEntryGuard |
| 212 | +
|
| 213 | +whenEntryGuard: 'if' {NL} expression |
| 214 | +``` |
| 215 | + |
| 216 | +Entries with a guard may only appear in `when` expressions with a subject. |
| 217 | + |
| 218 | +The behavior of a `when` expression with guards is equivalent to the same expression in which the subject has been inlined in every location, `if` has been replaced by `&&`, the guard parenthesized, and `else` by `true` (or more succinctly, `else` if` is replaced by the expression following it). The first version of the motivating example is equivalent to: |
| 219 | + |
| 220 | +```kotlin |
| 221 | +fun render(status: Status): String = when { |
| 222 | + status == Status.Loading -> "loading" |
| 223 | + status is Status.Ok && status.info.isEmpty() -> "no data" |
| 224 | + status is Status.Ok -> status.info.joinToString() |
| 225 | + status is Status.Error && status.problem == Problem.CONNECTION -> |
| 226 | + "problems with connection" |
| 227 | + status is Status.Error && status.problem == Problem.AUTHENTICATION -> |
| 228 | + "could not be authenticated" |
| 229 | + else -> "unknown problem" |
| 230 | +} |
| 231 | +``` |
| 232 | + |
| 233 | +The current rules for smart casting imply that any data- and control-flow information gathered from the left-hand side is available on the right-hand side (for example, when you do `list != null && list.isNotEmpty()`). |
| 234 | + |
| 235 | +### Style guide |
| 236 | + |
| 237 | +Even though `if` delineates the `whenCondition` part from the potential `guard`, there is still |
| 238 | +a possibility of confusion with complex Boolean expressions. For example, the code below may |
| 239 | +be wrongly as interpreted as taking the branch for both `Ok` status with an empty `info` |
| 240 | +or `Error` status; when the reality is that the second part of the disjunction is always false, |
| 241 | +since the guard is only checked if the condition (in this case, being `Ok`) is satisfied. |
| 242 | + |
| 243 | +```kotlin |
| 244 | +when (status) { |
| 245 | + is Status.Ok if status.info.isEmpty() || status is Status.Error -> ... |
| 246 | +} |
| 247 | +``` |
| 248 | + |
| 249 | +We strongly suggest writing parentheses around Boolean expressions after `if` |
| 250 | +when they consist of more than one term, as a way to clarify the situation. |
| 251 | + |
| 252 | +```kotlin |
| 253 | +when (status) { |
| 254 | + is Status.Ok if (status.info.isEmpty() || status is Status.Error) -> ... |
| 255 | +} |
| 256 | +``` |
| 257 | + |
| 258 | +Another option would have been to mandate parentheses in every guard. There are three reasons |
| 259 | +why we have chosen the more liberal approach. |
| 260 | + |
| 261 | +1. For small expressions, it removes some clutter; otherwise, you end up with `) -> {` in the branch, |
| 262 | +2. Other languages (like Java or C#) do not require them (although most of them use a different keyword than regular conditionals), |
| 263 | +3. People can still use parentheses if they want, or mandate them in their style guide. But the converse is not true. |
| 264 | + |
| 265 | +### The need for `else` |
| 266 | + |
| 267 | +The current document proposes using `else if` when there is no matching in the subject, only a side condition. |
| 268 | +One seemingly promising avenue dropping the `else` keyword, or even the whole `else if` entirely. |
| 269 | +Alas, this creates a problem with the current grammar, which allows _any_ expression to appear as a condition. |
| 270 | + |
| 271 | +```kotlin |
| 272 | +fun weird(x: Int) = when (x) { |
| 273 | + 0 -> "a" |
| 274 | + if (something()) 1 else 2 -> "b" // `if` here is not a guard, but a value to compare `x` with. |
| 275 | + else -> "c" |
| 276 | +} |
| 277 | +``` |
| 278 | + |
| 279 | +### Alternative syntax using `&&` |
| 280 | + |
| 281 | +We have considered an alternative syntax using `&&` for non-`else` cases. |
| 282 | + |
| 283 | +```kotlin |
| 284 | +fun render(status: Status): String = when (status) { |
| 285 | + Status.Loading -> "loading" |
| 286 | + is Status.Ok && status.info.isNotEmpty() -> status.info.joinToString() |
| 287 | + is Status.Error && status.isCritical -> "critical problem" |
| 288 | + else -> "problem, try again" |
| 289 | +} |
| 290 | +``` |
| 291 | + |
| 292 | +This was considered as the primary syntax in the first iteration of the KEEP, |
| 293 | +but we have decided to go with uniform `if`. |
| 294 | + |
| 295 | +The main idea behind that choice was the similarity of a guard |
| 296 | +with the conjunction of two conditions in `if` or `when` without a subject. |
| 297 | + |
| 298 | +```kotlin |
| 299 | +when (x) { is List if x.isEmpty() -> ... } |
| 300 | + |
| 301 | +// as opposed to |
| 302 | + |
| 303 | +if (x is List && x.isEmpty()) { ... } |
| 304 | +when { x is List && x.isEmpty() -> ... } |
| 305 | +``` |
| 306 | + |
| 307 | +However, that created problems when the guard was a disjunction. |
| 308 | +The following is an example of a branch condition that may be |
| 309 | +difficult to understand, because `||` usually has lower priority |
| 310 | +than `&&` when used in expressions. |
| 311 | + |
| 312 | +```kotlin |
| 313 | +is Status.Success && info.isEmpty || status is Status.Error |
| 314 | +// would be equivalent to |
| 315 | +is Status.Success && (info.isEmpty || status is Status.Error) |
| 316 | +``` |
| 317 | + |
| 318 | +The solution -- requiring the additional parentheses -- creates |
| 319 | +some irregularity in the syntax. |
| 320 | + |
| 321 | +A second problem found was that `&&` created additional ambiguities |
| 322 | +when matching over Boolean expressions. In particular, it is not clear |
| 323 | +what the result of this expression should be. |
| 324 | + |
| 325 | +```kotlin |
| 326 | +when (b) { |
| 327 | + false && false -> "a" |
| 328 | + else -> "b" |
| 329 | +} |
| 330 | +``` |
| 331 | + |
| 332 | +On the one hand, `false && false` could be evaluated to `false` and then |
| 333 | +compared with `b`. On the other hand, `b` can be compared with `false` first, |
| 334 | +and then check the guard -- so the code is actually unreachable. |
| 335 | + |
| 336 | +## Potential extensions |
| 337 | + |
| 338 | +### De-duplication of heads |
| 339 | + |
| 340 | +The examples used throughout this document sometimes repeat the condition |
| 341 | +before the guard. At first sight, it seems possible to de-duplicate some of |
| 342 | +those checks, so they are performed only once. In our first example, we |
| 343 | +may de-duplicate the check for `Status.Ok`: |
| 344 | + |
| 345 | +```kotlin |
| 346 | +fun render(status: Status): String = when (status) { |
| 347 | + Status.Loading -> "loading" |
| 348 | + is Status.Ok -> when { |
| 349 | + status.info.isEmpty() -> "no data" |
| 350 | + else -> status.info.joinToString() |
| 351 | + } |
| 352 | + is Status.Error if status.problem == Problem.CONNECTION -> |
| 353 | + "problems with connection" |
| 354 | + is Status.Error if status.problem == Problem.AUTHENTICATION -> |
| 355 | + "could not be authenticated" |
| 356 | + else -> "unknown problem" |
| 357 | +} |
| 358 | +``` |
| 359 | + |
| 360 | +Two challenges make this de-duplication harder than it seems at first sight. |
| 361 | +First of all, you need to ensure that the program produces exactly the same |
| 362 | +result regardless of evaluating the condition once or more than once. While |
| 363 | +knowing this is trivial for a subset of expressions (for example, type checks), |
| 364 | +in its most general form a condition may contain function calls or side effects. |
| 365 | + |
| 366 | +The second challenge is what to do with cases like `Status.Error` above, |
| 367 | +where some conditions fall through to the `else`. Although a sufficiently good |
| 368 | +compiler could generate some a "jump" to the `else` to avoid duplicating some |
| 369 | +code, the rest of the language would still need to be aware of that possibility |
| 370 | +to provide correct code analysis. |
| 371 | + |
| 372 | +### Abbreviated access to subject members |
| 373 | + |
| 374 | +We have considered an extension to provide access to members in the subject within the condition. |
| 375 | +For example, you would not need to write `status.info.isNotEmpty()` in the second of the examples. |
| 376 | + |
| 377 | +```kotlin |
| 378 | +fun render(status: Status): String = when (status) { |
| 379 | + Status.Loading -> "loading" |
| 380 | + is Status.Ok if info.isNotEmpty() -> status.info.joinToString() |
| 381 | + is Status.Error if isCritical -> "critical problem" |
| 382 | + else -> "problem, try again" |
| 383 | +} |
| 384 | +``` |
| 385 | + |
| 386 | +We have decided to drop this extension for the time being for three reasons: |
| 387 | + |
| 388 | +- It is somehow difficult to explain that you can drop `status.` from the condition, but not in the body of the branch (like the second branch above shows). However, being able to drop `status.` also in the body would be a major, potentially breaking, change for the language. |
| 389 | +- It is not clear how to proceed when one member of the subject has the same name as a member in scope (from a local variable or from `this`). |
| 390 | +- The simple translation from guards to `when` without subject explained in the _Technical details_ section is no longer possible. |
0 commit comments