It has been a little too long since my last post, but it's been a busy couple of months as I've been pushing hard to get some of my side projects moving along. Be on the lookout in the future as I plan to start going into more Android focused posts.
If you're familiar with C your mind probably just jumped (pun intended) to a bad place. Don't worry though, Kotlin's
implementation isn't anything as divisive as
Dijkstra's or
Rubin's
arguments. Kotlin locks its jump implementation to loops and return
ing from functions, so there isn't much of a
reason to worry about abnormal or undefined behavior. However, it's still good to give a little scrutiny to your
implementation before deciding to use a jump inside of a loop.
So lets start with a scenario. We have two ranges, we want to know when the value of range B exceeds the index of range A for the first time when looping over B size of A times:
fun loopy() : String {
for (rangeAVal in 4 downTo 0) {
for (rangeBVal in 2 downTo 0) {
if (rangeBVal > rangeAVal) {
return "rangeBVal: $rangeBVal, rangeAVal: $rangeAVal"
}
}
}
return "A is always >= B"
}
In the above example we're looking at a very simple nested for
loop with the outer loop looping from 4 to 0 and the
inner loop looping from 2 to 0. If rangeBVal
is greater than rangeAVal
then we return those values as a string. The
output will be:
rangeBVal: 2, rangeAVal: 1
This is fine, and in many cases may be what you want, but what if you wanted to parse the return values in the same function? You'd either need to break twice or (worse) finish both loops.
So how can we simplify this?
Kotlin's jumps are a great way to simplify our previous example. From the Kotlin documentation, Kotlin has three jump expressions:
return
. By default returns from the nearest enclosing function or anonymous function.break
. Terminates the nearest enclosing loop.continue
. Proceeds to the next step of the nearest enclosing loop.
This looks pretty similar to most languages, right? Well, Kotlin adds the ability to place labels to better direct
your control flow in these situations. You can label a loop using the [label name]@
syntax and
then jump to that label using the @[label name]
syntax. More simply, labels are defined to the left of @
and
referenced to the right of @
.
Knowing this now, how can we achieve our desired outcome with Kotlin's jumps? Easy:
fun loopyBreak() {
var printValue = ""
loopA@for (rangeAVal in 4 downTo 0) {
for (rangeBVal in 2 downTo 0) {
if (rangeBVal > rangeAVal) {
printValue = "rangeBVal: $rangeBVal, rangeAVal: $rangeAVal"
break@loopA
}
}
}
println(printValue)
// parse or modify printValue below as necessary...
}
Try to think about what's happening before you continue reading. Hint: the output will now be:
rangeBVal: 2, rangeAVal: 1
Notice the loopA@
label and the break@loopA
jump. Normally we'd have to do this in another function and return
printValue
or, maintain some state in loopA
to determine if our condition in the inner loop evaluated
to true so we could then also break
from loopA
. However, by leveraging Kotlin's ability to jump to a label, we set
our value in the inner loop and then initiate a break
from loopA
while still in the inner loop, giving us our
desired printValue
.
Grouping it together
So now that we have a basic understanding of jumps in Kotlin, lets demonstrate a series of use cases:
fun forEachLoops() {
val range = 0..4
println("return to label `loop@`")
run loop@{
range.forEach {
if (it == 2) return@loop
print("$it ")
}
}
println("\nreturn in loop to the enclosing `forEach`")
range.forEach {
if (it == 2) return@forEach
print("$it ")
}
println("\nreturn to outer loop at `loop@` from nested loop")
range.forEach loop@{
range.forEach {
if (it == 2) return@loop
print("$it ")
}
}
println("\nreturn from function")
range.forEach {
if (it == 2) return
print("$it ")
}
throw RuntimeException("We shouldn't reach this")
}
The print statements for each of these cases should be pretty self explanatory, but if you'd like to step through what is happening, the output of this function is:
return to label `loop@`
0 1
return in loop to the enclosing `forEach`
0 1 3 4
return to outer loop at `loop@` from nested loop
0 1 0 1 0 1 0 1 0 1
return from function
0 1
It's pretty easy to see the advantage here, especially when looping inside of lambdas like forEach
where break and
continue statements are not allowed.
Powerful but not always best
Jumps are powerful, but make sure you're using the right tool for the right job. If you run into a situation where you believe the use of labels and jumps is necessary, closely evaluate what you're doing. For many purposes, the original example is probably close to the best way to implement the scenario it was designed to solve. The use of the label and jump saved us from having two different functions, but at the cost of readability and maintainability. For example:
// original example
fun loopy() : String {
for (rangeAVal in 4 downTo 0) {
for (rangeBVal in 2 downTo 0) {
if (rangeBVal > rangeAVal) {
return "rangeBVal: $rangeBVal, rangeAVal: $rangeAVal"
}
}
}
return "A is always >= B"
}
// an optimized version of loopy...
fun loopyOptimized() : String {
val rangeA = (4 downTo 0)
val bMax = 2
rangeA.forEach {
if (bMax > it)
return "rangeBVal: $bMax, rangeAVal: $it"
}
return "A is always >= B"
}
fun callingFunction() {
val loopyResult = loopy()
// handle loopyResult
println(loopyResult)
}
By encapsulating the two for
loops into a single function (loopy
) and returning the value from within the loop, we
abstract the logic from callingFunction
, which makes it easier to read and both functions easier to test in
isolation. Additionally, once we get around to refactoring loopy
to loopyOptimized
, we'd only have to replace and
test the logic of the looping function and not callingFunction
, just which makes automated testing easier, and code
reviews significantly easier.
This isn't to say labels and jumps aren't a powerful tool that should be utilized, but as with all new tools, we need to ensure we understand when to use them and when we shouldn't.