Skip to content

Commit ddfe515

Browse files
authored
Guards
2 parents 8f4f57b + 0305a7f commit ddfe515

File tree

1 file changed

+390
-0
lines changed

1 file changed

+390
-0
lines changed

proposals/guards.md

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

Comments
 (0)