7

Swift’s Guard Statement for Kotlin

 3 years ago
source link: https://medium.com/swlh/swifts-guard-statement-for-kotlin-967ba580443f
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

Swift’s Guard Statement for Kotlin

Image for post
Image for post
https://unsplash.com/photos/znMu0Enj_Gw

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, a guard statement always has an else clause—the code inside the else 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 an if 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!


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK