Why I missed semicolons today
Coding in Kotlin has mostly been an amazing journey so far. The expressiveness of the language and the drastic cut of boilerplate code compared to Java (which I used to love) has made my coding so much more enjoyable. Not needing semicolons anymore was one of those little changes that made my life easier. Until today.
As I was practicing my skills with today’s puzzle from the advent of code, I wrote a test case that would not pass. A certain function was not returning the proper result. Even more confusing was the fact that selecting the return expression in Intellij and having it evaluate the result gave me the expected value. But running it ended up with a different one.
Basically, it looked something like this:
@Test
fun `should be 4`() {
val anExtremelyLongAndBoringStatementThatBarelyFitsOnALine = 2
val anotherExtremelyLongStatementThatBarelyFitsOnALine = 2
val someList = listOf(1)
val result = someList.map {
anExtremelyLongAndBoringStatementThatBarelyFitsOnALine
+ anotherExtremelyLongStatementThatBarelyFitsOnALine
}
Assert.assertEquals(listOf(4), result)
}
Reading this test seems obvious: I’m transforming the content of a list by replacing each of its elements with an addition of two statements. Since those statements are a bit long, I split the whole thing on two lines and indented it to make it more legible. So should that test pass? You might be tempted to say yes: we’re adding two long statements, and each of them equals 2. As far as I’m concerned, 2 + 2 = 4. We should be fine.
Well, Kotlin decided otherwise. If you try to run this code, you’ll see that it fails: the result list contains a single element with the value 2, not 4! I tried to debug my code, added temporary variables to check each step, and even the “evaluate expression” in Intellij agreed with me that this addition should return 4, but still, the test would not pass.
So what happened here? Well, it’s an accumulation of little details:
- I didn’t need the return keyword inside my lambda expression.
- I split my addition on several lines for clarity’s sake.
- Kotlin doesn’t need a marker like a semicolon to identify valid statements and expressions.
A first clue would be to reformat automatically your code: you’ll notice that the nice indentation is lost. Both long halves of the addition are lined up, and the space between the + sign and the rest of the line is lost. Next clue is that moving the + sign up at the end of the first line instead of the beginning of the second solves the problem.
Do you see it now? The root of the problem is that this + sign can be both the binary addition operator and the unary positive sign operator. And because Kotlin doesn’t need semicolons, it tries to isolate valid statements based on other inferences, and it turns out that both lines are valid statements in their own right. From the compiler’s point of view, my lambda expression doesn’t contain a single expression, but two expressions, the second one starting with a unary plus!
Since we don’t use return in lambdas, the last statement (which is only half of my addition) is used. The same mistake inside a proper method would have been noticeable because the compiler would have given me a warning:
fun addition() {
return anExtremelyLongAndBoringStatementThatBarelyFitsOnALine
+ anotherExtremelyLongStatementThatBarelyFitsOnALine
}
The second half not being included in the return statement, it would be considered unreachable code. And as I stated in my introduction, that same error would not happen in Java because the compiler would not stop and assess the statement until it reached a semicolon.
So as a conclusion, be very careful when you split expressions on several lines. It might make your code easier to read, but it could also have unexpected side effects, especially with operators like + or - that can be considered both as binary and unary operators. This is not limited to numbers: don’t forget strings, lists, or any other class for which you overload these operators.