Swift’s Guard Statement for Kotlin
Even if you live on the Kotlin side of things maybe once in a while you also check Swift code. If you do, you probably noticed how similar both languages are. To me, Kotlin looks more concise but I’m also biased as I work with Kotlin daily. On the other hand, Swift has some great features too. One is the guard
keyword, a really nice tool Swift developers have that we are missing. Can we bring it to Kotlin?
What is guard?
guard
is used to exit a block of code early, if a given condition is not met. This way all the code following the guarded variable can be safely executed.
Let’s look at an example where we do some calculation but only if the given input can be converted into an integer:
func printResultFor(input: String) -> Void {
guard let result = Int(input) else {
println("input was not an integer")
return
}
// function continues with valid int
print("result: ", 100 * result)}
Of course, simple code like this could be written with the standard if
/else
statements!
But the power of guard
becomes apparent when many of these statements are used in succession. Written in a traditional way this can easily lead to a hell of nested ifs where you make sure everything is safe in the innermost block when all the if
are true.
Take a simple sign up flow for example, where the customer needs to enter user, password, and their age for validation. We have to make sure those are not nil
or empty and check for a valid age.
func submit(usernameText: String?, passwordText: String?, ageText: String?) { guard let username = usernameText, !username.isEmpty else {
print("username is not set or blank")
return
} guard let password = passwordText, !password.isEmpty else {
print("password is not set or blank")
return
} guard let ageString = ageText else {
print("age is not set")
return
} guard let age = Int(ageString), age > 18 else {
print("age not valid")
return
} // all values are checked and valid here
register(username, password, age)}
Here the register
method is only called once all the criterias are met.
How would we build this with Kotlin?
Version 1
Let’ go back to our original simple validation and start with something like:
fun printResultFor(input: String) {
val result = input.toIntOrNull() ?: run {
println("input was not an integer")
return
}
println("result: ${100 * result}")
}
This is the easiest way to implement early return in Kotlin (as also shown in this post).
Actually thanks to the way every expression in Kotlin evaluates to something, we could actually implement this also with a traditional if
/else
:
val result = if (input.toIntOrNull() != null) input.toInt() else {
println("input was not an integer")
return
}println("result: ${100 * result}")
The compiler detects that the else
case exits earlier and as of that we get a valid result
variable.
Both those variants (run
+ if
/else
) work fine as long as we can return
from a function.
Is there any other way?
How else could we get a function that shows the compile a correct type, but actually never returns? A simple (but heavy) way to achieve that is by simply throwing an exception:
fun <T> shouldNotHappen(function: () -> String): T {
println(function())
throw IllegalArgumentException(function())
}
Now the usage can be:
fun printResultFor(input: String) {
val result = input.toIntOrNull() ?: shouldNotHappen {
"input was not an integer"
}
println("result: ${100 * result}")
}
Maybe let’s give it a better name, how about otherwise
? (unfortunately else
won’t work here like in Swift).
fun printResultFor(input: String) {
val result = input.toIntOrNull() ?: otherwise {
"input was not an integer"
}
println("result: ${100 * result}")
}
As you probably noticed, this is not the same anymore as with what we started:
First, it’s not clear that this block throws an exception.
Second, we don't want an exception to be thrown!
What we wanted, was to pass any arbitrary lambda as action when the condition is not met.
Version 2
We can achieve this by adding another function around it that can do the catch and the handling (we’ll mark it inline
to avoid additional costs):
fun <T> otherwise(function: () -> Any): T {
throw GuardedException(function)
}inline fun <T> guarded(guardedBlock: () -> T) {
try {
guardedBlock()
} catch (e: GuardedException) {
e.guardBroken()
}
}class GuardedException(val guardBroken: () -> Any) : Exception()
Using this would look like:
fun printResultFor(input: String) {
guarded {
val result = input.toIntOrNull() ?: otherwise {
println("input was not an integer")
}
println("result: ${100 * result}")
}
}
This looks nice! We are nearly there!
I think it would be nice to mark otherwise
in a way that it’s clear that it will never return. Kotlin has the type Nothing
for this!
fun otherwise(function: () -> Any): Nothing {
throw GuardedException(function)
}
Et voilà! We are done!
Check out this version from Aidan Mcwilliams with who came up with this approach. You might also want to check out the library from Nathanael who uses a similar idea to find errors in his code
A different approach
I’d like to explore another approach though: is there a way to mark the otherwise
optional without losing the safety?
It would be nice to write something like:
guarded {
val result1 = guard{ input1.toIntOrNull() }
val result2 = guard{ input2.toIntOrNull() }
println("result: ${100 * result1 * result2}")
}
Here we have two guarded blocks and we would only want the println
statement when both of them succeed.
This looks nice but actually violates how guard
is used in Swift:
Unlike an
if
statement, aguard
statement always has anelse
clause—the code inside theelse
clause is executed if the condition is not true.
But it might be handy nevertheless. We can still have the alternative block but make it optional:
guarded {
val result1 = guard{ input1.toIntOrNull() }
val result2 = guard(guardedBlock = { input2.toIntOrNull() }) {
println("input was not an integer") } println("result: ${100 * result1 * result2}")
}
To achieve this, we just need slight modification, creating a guard
function with that optional 2nd parameter:
fun <V> guard(
guardedBlock: () -> V?,
alternativeBlock: () -> Any = {}): V
= guardedBlock() ?: throw GuardedException(alternativeBlock)
Wasn’t that hard, wasn’t it?
One more modification
Our examples have one flaw, they rely on nullability while Swifts guard
handles booleans expressions. To quote the documentation:
A
guard
statement, like anif
statement, executes statements depending on the Boolean value of an expression.
Achieving this is a bit more complex but we can continue on what we built above.
As we now need to return a Boolean
from our function while keeping the original value, we’ll make guard
an extension function of the instance itself.
So instead of
guarded {
val result1 = guard{ input.toIntOrNull() }
// ...
}
we’ll write:
guarded {
val result1 = input.guard({ isNotBlank() }).toInt()
// ...
}
Notice how we can call toInt
safely because of the guard.
Our guard functions changed to:
fun <V> V?.guard(
guardedBlock: V.() -> Boolean?,
alternativeBlock: () -> Any = {}) =
if (this != null && guardedBlock(this) == true)
this else throw GuardedException(alternativeBlock)
Looks more complicated than it is. We just made it an extension function on our value. And depending on the Boolean
result of the guardedBlock
it returns the value itself or throws the exception as before.
With this we can write:
guarded {
val username = usernameText.guard(String::isNotEmpty) {
println("username is blank")
}
val password = passwordText.guard(String::isNotEmpty) {
println("password is blank")
}
val ageString = ageText.guard(String::isNotEmpty) {
println("age is empty")
} // ...
}
If we want we can make use of infix
declaration to make this a bit nicer and also bring back the otherwise
infix fun <V> V?.guard(block: Pair<V.() -> Boolean?, () -> Any>) = // as beforeinfix fun <V> (V.() -> Boolean?).otherwise(that: () -> Any)
= this to that
which leads to:
guarded {
val username = usernameText guard(String::isNotEmpty otherwise {
println("username is blank")
})
val password = passwordText guard(String::isNotEmpty otherwise {
println("password is blank")
})
val ageString = ageText guard(String::isNotEmpty otherwise {
println("age is empty")
}) // ...
}
Conclusion
As you saw Kotlin as many concepts you can play with to achieve something comparable.
Of course, without an official language keyword, it’s not possible to have the same design as in Swift. But I hope I encouraged you to try to rebuild things you see in other languages and even if it’s just about practicing your own language with this.
PS: Thanks for all the reviewers that I gathered within a few minutes on Twitter! You are amazing and helped me formalizing and improving these thoughts
PPS: Feel free to comment with even more variations!