Feature request: Use infix *if* operator to emphasize non-local control transfer

This is inspired by Ruby’s inline variant of the if operator.
E.g. consider the following ‘real life’ code:

def solution(k, m, a)
    return [] if a.empty?
  
    freq = Hash.new(0)
    a.each { |x| freq[x] += 1 }
  
    n = a.length
    if freq.size == 1
        return [a[0] + 1] if k > n/2.0
  
        return [a[0]] if k < n/2.0
  
        return []
    end
    ...

Ruby allows any expression/statement to be guarded by postfix if operator. I suspect Kotlin designers will resist such a frivolity.
But how about allowing postfix if operator only with statements that cause non-trivial transfer in the program’s control flow. Statements like return, throw, break, continue (any others that I am missing?)

1 Like

Ok, I am missing something – function invocation is a non-local control flow transfer. But then function invocations are … omnipresent.
And there’s a notable difference – function invocation transfers control ‘down the stack’ (i.e. we’re getting deeper but later we will return to the invocation site), whereas return and throw transfer it ‘up the stack’ never to return.
For that reason I’d suggest we keep function invocation out of the proposal.

I hope Kotlin designers will resist such a frivolity.

You suggest to preserve this

if (k > n/2.0) return a[0] + 1

and to add this

return a[0] + 1 if (k > n/2.0)

Do you see a concrete improvement?

1 Like

The only improvement I see is mentioned in the topic title – … to emphasize non-local control transfer.

In my subjective opinion this aids human readability, quicker and more effortless understanding of the code. The exit points of a function/loop/block tend to be important and emphasizing them seems a good idea.

In Ruby there’s the added value that a regular-if, canonically written is always at least 3 lines of code:

if k > n/2.0
    return a[0] + 1
end

hence inline-if in Ruby really saves a lot (and aids readability a lot).
This is not the case in Kotlin, as it can be as terse as that:

if (k > n/2.0) return a[0] + 1

I think there’s a negligible chance of accepting that proposal, but I am more interested in the grounds of turning it down.
Like for example, such a feature will not make it into Python, because of Python’s “There’s Only One Way To Do It” paradigm.

The reason I hope it gets turned down is that it makes code harder to read quickly.

Everything in Kotlin is prefix; this means that you can get a rough sense of the code from skimming the starts of lines. A postfix operator completely changes that; it means you can’t trust the start of a line, because something at the end of the line could suddenly make it conditional. That reverse order is something that keeps tripping me up when I read Perl code, and I would find it highly annoying and probably error-prone in Kotlin.

3 Likes

Well,
perhaps you’re failing to see it from this perspective: a plain if (without an else) has two components: a (guarding) condition and a (guarded) statement:

if (<condition>) <statement>

Actually, there is even a third (constant) component here – the if keyword itself.
So, unless our mortal human conceptual minds are hyper-threaded (and research mostly says they are not) you have to deal with those 3 components in certain order. Even when you are just skimming.
I suggest in the prefix-if case it first becomes evident “that’s an if!”, then the condition, and only then then the guarded statement (return-or-break-or-throw in our case).
Whereas, in the postfix-if case:

break if (<condition>)

the first thing that’s evident is that “We return (or break or throw) here!”, and then we see that this return-or-break-or-throw is conditional, and if we’re interested – the particular condition. And I claim we don’t even need to see the postfix-if in order to guess it is conditional, as we see with our peripheral sight there’s more code both on that line and perhaps on the next line(s), while an unconditional return-or-break-or-throw does not have any code neither to the right of it, nor right bellow it (it would be dead code).

Of course the ‘rationale’ above is nothing, compared to our stubborn habits. And I can totally relate to your situation, as I was approaching Ruby from a die-hard C++/Java-ist perspective. Initially my mind resisted it too. But I had to learn it and I did and now I find it quite convenient.

(We can call that story:)
Dr. Strangelove or: How I Learned to Stop Worrying and Love the Postfix-if Bomb

Similar postfix-conditionals are already supported in Kotlin in form of standard APIs:

return o.takeIf { condition }
return o.takeUnless { condition }

I am using those a lot and I really like them for similar reasons why the OP is advocating postfix-if.

Also, I am one who strongly advocates early returns, instead of nested if constructs. My functions often start with multiple lines, with an conditional return in each of them. Then after those lines the actual business logic for that particular function starts. The advantage of this style of coding is that the preliminary conditions can be skipped over, and the important part of the function can be found very quickly.

Example:

fun foo(a: A) {
    if (a.isBaz()) return 
    if (hardToComputeBooleanFor(a)) return
    if (someVeryComplexBooleanExpressionFor(a)) return
    
    a.nowDo()
    a.the()
    a.interesting()
    a.stuff()
}

The conditional returns are often not very interesting, and only distracting. Coding them like this is still better than using nested if constructs. Using the postfix-if-returna would further help quick skippability of those less interesting parts of the function.

2 Likes

That’s exactly my style - early returns and then you get to the meat of the function, which keeps nesting levels lower.

I contend that the more levels of nested constructs you have, the harder it gets for humans to quickly grasp what’s going on.

Thanks for posting that!

1 Like

Exactly.

And I often find that no matter how deeply a function is nesting its if statements, in almost all cases you can eliminate all nesting altogether by applying early returns.

if(freq.size == 1)
    return when
    {
        k > n/2.0 -> listOf(a[0] + 1)
        k < n/2.0 -> listOf(a[0])
        else -> listOf()
    }
1 Like
if(freq.size == 1) return when {
    k > n/2.0 -> listOf(a[0] + 1)
    k < n/2.0 -> listOf(a[0])
    else -> listOf()
}

Every line counts.

No actually they don’t. Lines of code is a meaningless metric. Much more important is readability, which you sacrificed greatly to reduce code by one line.

One of my favorite statements on the subject (which I often quoted like 15-20 years ago in the comp.lang.java.* news groups) was this one by Kent Paul Dolan:

Horizontal white-space is precious, vertical white-space is cheap. Screen widths are limited, in Sun’s convention to 80 characters…This can often be compensated for by use of vertical white-space, which exists in effectively infinite supply with scrolling text windows.

So no, every line doesn’t count as there is an infinite supply of vertical lines, they are free, they do not need to be unnecessarily rationed.

The whole point of this thread is trying to “emphasize non-local control transfer”, i.e. to make it more obvious that there is a break in linear code flow. Hiding the return after the if is exactly what the OP was trying to prevent.

3 Likes

2 lines actually :wink:

I was too sarcastic, sorry for that. Of course every line does not count. Reducing number of lines does not necessarily increase readability.

Having said that, what I have shown is indeed my style of programming, in particular cases like this one. Not always.

I understand your point. I had the same opinion 2 years ago, but my opinion changed.

While reducing number of lines is not my ultimate goal, the times where we squeezed our code in lines of 80 characters are long past in my opinion. We have much bigger displays nowadays and we can comfortably read more characters than that per line.

And I wasn’t arguing for 80 character lines, just preserving the quote. The original Java coding convention from Sun (which I disagreed with on many fronts) was indeed 80 characters. Now days 110-120 is more reasonable.

But no matter the number, the point is that horizontal space is limited, but there is no limit on vertical space and no reason to shoot for fewer lines of code unless that makes things more readable.

Similar to the OP, my coding convention is to emphasize cases where flow is not linear (return, if, for, while, break, etc.). My convention is that any of those must be separated from the code before and after it by a blank line (a brace on a line also qualifies). I also am I firm believer that hiding the open brace at the end of a line is insanity and is objectively provable to be harder to read.

I also try to emphasize them structurally. E.g. many people might do this:

if(...)
{
    throw ...
}

return ...

I would try to turn that into an if-else or a when to emphasize that the code may not reach the second return instead of relying on them to actually have to read the word throw to know that.

I agree absolutely.

I am often using empty lines, too, when I believe the code is better structured that way.

Here we start disagreeing then. :wink:

Or you could reduce some more lines of code here:

if (...) throw Exception(
    "log message"
)

return ...

/sarcasm :upside_down_face:

Jokes aside, as explained in a previous comment in this thread I am a big fan of early returns. Especially if the expression in the later return is very complex. I don’t add else blocks just for emphasizing that this block might not be reached. It increases chances of deeply nested code (now or in a future version of the function). I try to prevent nested code at all costs.

omg, stop suggest to turn kotlin in ruby|scala|python memorial - it’s already too flexible to read random junior code
no, please no

There’s a third way, which lines up the braces and avoids any extra vertical space:

if (...)
{   throw ...
}

I don’t know why more people don’t use that! (The only disadvantages are lack of IDE support, and the need to take the brace into account when adding/removing lines at the start of the block. But the latter has never been a problem for me.)

Why wouldn’t this be supported by your IDE? Ok, it’s not the default settings for kotlin but you can just tell idea that you want braces on separate lines.

I think it is important to ask ourselves why are we doing anything at all. Assuming that we cannot drop the braces, we could write it like this:

if(...) {
throw ...}

But the fact is that there is a non-linear flow here. Without making some visual distinction to aid the reader we are making it harder to read and understand the code, To increase the readability we want to make the distinction between the two lines as large as possible.

One way to do that is the way that many people think is sufficient enough distinction (called K&R style) like this:

if(...) {
    throw ...
}

This has increased the distinction, but not by very much. I think a telling piece of evidence that it is not a sufficient enough distinction is the number of times when the first line of the nested block is complex and perhaps spans multiple lines programmers will often add a blank line:

if(...) {

    A really complex expression
        that spans
        multiple lines
}

I would agree that your solution (which is actually called Horstmann style) is a tiny bit better than what I call the “hidden brace style”, but can we do even more to make the distinction as large as possible, which increases readability. The answer is called Allman or BSD Style. This maximizes the distinction between the control statement and nested statements:

if(...)
{
    throw ...
}

In all other brace styles the distinction is less than in Allman style, therefore all other styles are theoretically less readable. I think this could actually be proven using experiments testing a large group of developers to measure how quickly and accurately they are able to decode the various styles, but I am convinced enough without such experiments since I know that for me and a large number of people the other styles are indeed less readable.

The biggest issue I had with this notation in Java was that the following was valid on its own:

{
    // code ...
}

So by reading the beginning of your lines, you may possibly miss:

if (condition) return 5
{
    // code...
}

I have to admit that using this kind of block on its own is kinda bad practice (replaceable by a function call), and as fatjoe79 pointed out, Kotlin doesn’t execute standalone blocks like this (they are just unused lambdas).

However, another thing is that I prefer seeing the if (condition) as a sort of “function” applied to the given block (because it is technically applying a condition to the block), and that’s why the “bracket-at-end-of-line” is nice IMHO. It makes things look just like:

runIf(condition) {
    // code...
}

Which makes it beautifully consistent with functions with lambda.

We could argue that, to reach consistency, we could go the other way around and write:

listOf(1, 2, 3).mapTo(ArrayList())
{ 
    2 * it
}

But that would be confusing because we wouldn’t be able to tell if the first line ends there or if we need to read more without knowing the function’s signature.

Maybe this is a matter of habits, and maybe actually wrapping braces for both ifs and functions with lambdas would achieve decent readability when used to it.