Kotlin "Features", Compiler Lookahead, and Source Code Formatting

I realize that I am in the minority, but I have long preferred the GNU formatting style for indentation, where the semicolons are aligned and indented, as in

while (...)
  {
    ...
  }

if (...)
  {
    ...
  }

Kotlin has a nice feature that allows trailing lambda parameters to be moved outside the parentheses, but the compiler will not recognize the trailing lambda if I try to follow this formatting style. So, for example, the following are valid

str?.let { ... }   // all on one line

str?.let {         // left brace on same line as let
    ...
}

str?.let()         // use parentheses
  {
    ...
  }

str?.let(          // lambda inside parentheses
  {
    ...
  })

But when I try to omit the parentheses and format the trailing lambda using the GNU style, the compiler flags this as an error.

str?.let           // opening paren on next line
  {
    ...
  }

It seems to me that the compiler should be able to use an additional lookahead token to recognize that I have a lambda expression starting on the next line.

Similarly, there is another Kotlin feature that allows semicolons to be omitted. But if the text on a line constitutes a valid statement, then the compiler will treat it as such without looking at the next token to see if the statement is being continued. So, for example, when programming in Java I sometimes split long strings with a “+” on the next line, as in

String s = " ... "
         + " ... ";

But if I try to split the line similarly in Kotlin,

val s = " ... "
      + " ... "

the compiler will again fail to lookahead to see the “+” on the next line and will flag this as an error.

I really like Kotlin, but it is the only language I use that forces me to adopt a formatting style different from my personal choice. I realize that nothing I have said will change anything with respect to Kotlin, but I just needed to rant.

I don’t like the formatting you prefer (for reasons there’s little point in discussing; no-one ever won an argument over personal taste :slight_smile:). But I do agree that the way the Kotlin compiler infers semicolons is too aggressive.

AIUI, it infers a semicolon at the end of every line that would be valid with one. This prevents the style you demonstrate, but it also prevents other things, such as wrapping lines before operators. For example:

val v = "some long string, "
      + "another long string, "
      + "and another."

iI find this sort of wrapping much easier to follow; the operators line up neatly, and the code structure is obvious at a glance. But that’s not valid Kotlin (unless you put parens around the whole expression), because Kotlin infers a semicolon after the first line, even though the remaining lines make no sense that way.

Even worse:

val v = (/* some numeric expression */)
      + (/* another numeric expression)
      - (/* and another */)

This is valid Kotlin — but it doesn’t mean what it looks like! Kotlin infers a semicolon after the first line, setting v to the first expression. Then it treats the following line as a lone numeric expression, with a unary plus prefix: it evaluates the expression, but discards the result. And it does the same with the third line (treating it as starting with a unary minus).

That’s a really nasty gotcha…

As far as I’m concerned, the Kotlin compiler should infer a semicolon only when it’s necessary to make the line valid, not just whenever it’s possible.

I think that would be a safe change to make: it wouldn’t change the meaning of any existing valid code. But it would allow the sort of formatting we prefer. And it would prevent some very nasty gotchas…

1 Like

IMHO one’s personal style should always adapt to fit the standard. Style opinions are malleable and often harmful to hold rigidly between languages.
It looks unprofessional to put your style over your team’s.

This is a big change. I would expect plenty of DSLs will break and other breaking changes hard to find. It creates a new gotcha.

Without any change It’s easy to line up expressions

fun main() {
    val text = "hello string" +
               "Another string" +
               "A third string"
    print(text)
}

I would suggest that Kotlin shouldn’t go out of it’s way to support styles that diverge over the official style. While it would be nice to support styles for free but it’s not worth breaking changes.

4 Likes

Agreed. If there is a team or project style, then it should be followed. I never suggested otherwise.

Can you give a concrete example of a DSL that will break with such a change?

For example, imagine you created a builder of collection of some kind and you implemented unaryPlus operator to allow DSL like this:

myCollection {
    + "hello"
    + " "
    + "world"
}

As a matter of fact, kotlinx.html did something similar. In this case it is not clear if it should call unaryPlus() 3 times or only once with concatenated string.

But I agree this is a rather rare case and I can’t find anything else from the top of my head.

1 Like

Definitely. Hopefully that didn’t come off with a negative tone either :slight_smile:

The main part I’d consider a major change was the compiler including the next line greedy-style in all cases.
But thinking through a few of the use cases I’m realizing it might not be as much of a change as I thought.

My instinct is to say the compiler should always infer the end of the statement at the end of the line unless it wouldn’t be valid (which isn’t correct either). However that would fix the issue of putting the brackets on the next line in

str?.let
{
   // ...
}

Here are a few top-of-my-head scratch notes:

fun main() {
    
    val a = bar // prop
    val b = bar // prop. Expect func if greedy
    {}
    val c = bar() // func, this is greedy
    
    {}
    val e = buzz("param") // double, this is greedy
    
    {}
    
    //--- Errors. Expect to work if greedy
    //val x = baz
    //{}
    
    Unit to Unit
    //--- Errors. Expect to work if greedy
    //Unit
    //to
    //Unit
    
    //--- Returns Unit. Expect to work if greedy
    //return
    //"return".also { println("return") }
    
    if 
    (
        false
    )
    
    println("if") // Works, should not print since greedy
    
    
    //--- Errors. Expect to work if greedy
    //println
    //("Hello")
    
    //--- Errors. Would print "extension" if greedy
    //println("Unit?") // is not greedy
    //()
    
    // Works, not greedy
    val println = "prop print".also { println("prop print") }
    println
    ("func print")
    
    // Works, this is greedy
    when
    (true)
    {
        true -> println("when")
        else -> null
    }
    
}


val bar: String
    get() = "BAR".also { println("prop") }
fun bar(block: () -> Unit) = println("func")
fun baz(block: () -> Unit) {
    println("func")
}
fun buzz(p: String) = println("single")
fun buzz(p: String, f: () -> Unit) = println("double")

operator fun Unit.invoke() = println("extension")

I guess I expect there to be a bunch of corner case changes with things like invoking, infix functions, and more if there was a blanket rule for consuming the next line.

Thanks for the examples. As one who programs frequently in Java, Kotlin, C#, and occasionally in C++, I am troubled by the fact that

val = 5
   + f(6)
   - f(1)

compiles but doesn’t do what I think it does, and the fact that

val b = bar {
}

compiles differently from

val b = bar
{}

It’s just not what I am used to with programming languages using a C-like syntax. I guess the bottom line is that one must use Kotlin’s recommendations for formatting source code to be safe, especially when using trailing lambdas.

1 Like

You luckily never used Pyton or Haskell, then :smiley:

1 Like

There’s a “gotcha” either way
A lot of these are cases where the code could do either what you expect or something else. These ‘gotchas’ mean that no matter what the behavior the ambiguity could cause confusion.

To avoid confusion we can:

  1. code in the standard style to bring clarity
  2. code with some explicitly-ness added in to bring clarity

#2 is interesting because it keeps consistency. For example, this style forces consumption of the lambda and throws an error if it’s too many args:

bar()
{} // Always treated as an arg

The downside with trying supporting consistency for both styles (what OP is asking for) that is that it sacrifices consistancy when combined with some language choices. Trailing lambdas and implicit semicolons are features that run counter to next-line styling.

I can see an argument for limited changes. I’m mostly speculating on there being more corner cases that would be annoying to solve. You’d probably still need a version bump in Kotlin to change the grammar right?


{ A bit of a word spill here. It’s related but enough of a side topic that I’m hoping to keep it from being a distraction :slight_smile: }

Compiler choices and style rant

This is one of several places code would get ambiguous.
The compiler’s interpretation switching to match interpretation, and favor including the next-line, is equally reasonable compared the one statement interpretation. No matter what there is a “gotcha”. If we must pick an interpretation to favor, shouldn’t the official style be favored?

Sacrafice consistency might be okay?

With that said I think you might have a stronger argument on very specific cases. Let’s look at the parathesis requirement for trailing lambdas on the next line.

In favor of forcing parentheses:
The compiler is helping you write consistent code by forcing you to explicitly choose the less-kotlin-like interpretation (the less common one) and won’t compile any other way when you write parathesis-\n-lambda.
The compiler errors on

println("")
{} // Error, this is consistently seen as an argument even though both lines could be valid statements

One could argue that it’s pragmatic to handle this case conditionally. Like when one statement is invalid. This is what I and gidds were originally suggesting just on different criteria. One could argue it’s worth limited sacrifice of the consistency–limiting to very specific cases might make the argument stronger.

Style isn’t always subjective

I want to make the case that it’s okay for a language’s official style to be favored, and for languages to not support a wide range of styles.

For example, Kotlin does not support next-line style to the same degree as Java. When the compiler is given equally valid options for interpreting statements. The compiler favors the interpretation that lines up with the official style.

For many languages, syntax features will be implemented favor a style. Sometimes language features enable more flexibility (i.e. Java ignoring all whitespace). Other languages will not allow that at all flexibility (which is just syntax at that point) such as Python using whitespace scope.

Kotlin is somewhere in the middle–it is flexible but requires slight code edits for style changes. Features like trailing lambdas and implicit semicolons force style to tie into language features. Kotlin supporting fewer styles enables more freedom in implementing these features.

A style being supported could be thought of as a language feature. It puts limits on future features. Seeking to support an additional style, such as ignoring all whitespace, comes at a huge cost.

Always prefer the standard, even in practice

I like the saying “we code for humans, not for computers” because it highlights that the top priority of source code is clear communication with humans. In this context, I’d add that “we code for other humans, not for ourselves”. Code is about communication–it’s the reason we can say one should always prefer your team’s style. In the same vain, your team should prefer the community’s style over its own.

I’d suggest that even in your own private code, which will never be seen by another human, you should still code in the official style. The reason being is that if one wishes to communicate clearly in any language/dialect, they should try to communicate as the native speakers do. They should practice and become comfortable with the idioms, grammar, structure of the words as it is spoken by the community.

Yes It’ll be uncomfortable at first but personal taste is malleable. Even if there isn’t enough time to un-due one’s discomfort in the style, that’s okay. At least they communicated clearly.

Don’t lead your team into your dialect

Choosing your teams style is good for communication, but what if your preferred style is chosen?

I have seen some people get away with making their team choose their style. This person liked to space their code differently with spacing and alignment. They also valided consistancy and the team adapted their dialect. They got their preferred style and consistency!

IMHO this is putting one’s preference over your teams needs to communicate in the larger community and their project’s/team’s future. Team members change. Code doesn’t live in isolation. Making a team adapt a preference that’s against the grain is not good for the team.

Teams sometimes respond with “it’s just preference, we can appease your style needs” to keep the top developer happy–but that’s an unfortunate scenario. I’d hope all of the developers, especially the most valuable ones would seek the best for their project and team even if it means some discomfort in style.

1 Like

What about this then:

    val x = "123"
        .toInt()

Here, the compiler doesn’t infer a semicolon after the line val x = "123", even though that line can stand on its own, making x a String. But in fact the compiler looks ahead to the next line and makes x an Int.

Here’s a link to the Kotlin Grammer

Kotlin doesn’t do an “always assume when X is valid/invalid” or “only assume when X is valid/invalid”.

Examples like this don’t conflict with the Kotlin Grammar, it does show that it isn’t as simple as “check if the line is valid”. IMHO those ideas were brought up more for coder expectations (and intuition) for how the line would be parsed, and if (or if not) the parsing is different from that expectation often.

1 Like

Actually I have used Python and Haskell briefly in the past, but I don’t use them on a regular basis. I strongly prefer Java, Kotlin, and C#.

Here is my summary of this discussion: Kotlin “sometimes” looks at the next line to decide whether or not it is a continuation of the current statement/expression. Personally, I would prefer the consistency of semicolons to mark the end of a statement/expression. Then there would be no special cases to remember.

1 Like

The Kotlin desigers assume “Java-style braces” quite explicitly in the docs: