Any reason to not keep the good old for loop format?

I don’t get it, why changing something that is no need to be fixed?
There is no need to replace the old for loop structure, when you can just overload the old one.
It is so troublesome to translate the existing for loop with all those keyword (rangeTo, downTo, until and a missing until-reverse) , when you can just use > < >= <= to replace those tedious long phase.

I missed the old for loop…

 for (i = 1; i < 10; i++) {
 }

Personally I don’t miss the old loops at all. For me the new ones are much better at conveing developer’s intentions. I don’t also need to worry then wherever I need to use < or <= in conditions or incrementing values manually. Some examples:

  • iterate through array/list indexes: for (index in array.indices)
  • iterate through array/list indexes and values: for ((index, value) in array.withIndex())
  • iterate from i to j: for (n in i..j)
  • repeat operation n times: for (repetition in 1..n) or simply repeat(n)

I’m open for discussion, but there are probably not many common cases where classic loops would work better :slight_smile:

10 Likes

There is not a better way in styles perspective. It is about the compatibility and familiarity for programmer across all other languages (C, C++, java, JS etc.). It means more when Kotlin suppose to be a language that focus on java compatibility.

On the other hand, Comparison and Logical Operators are what typical programmer are familiar of. Removing that option entirely doesn’t seem reasonable to me. For example, in JS, they have different for method, such as forEach, for/in, for/of. Every programmer can choose their own way of writing their code.

This is highly opinionated. And my personal opinion is that it is ugly as hell :wink:

If you talk to a friend, you will probably say: “Let’s iterate from 2 to 6” and not: “Let’s create a counter, initially set it to 2, then with each iteration increment it by one, each time checking if it is smaller or equal to 6”. It should be the same with programming languages.

In practice, there are mainly two reason why we need to loop. We either iterate over something, including a range of numbers or we have some custom looping logic. For the first case working with counters is just more convoluted way to say that we are iterating over something. For the second case for is sometimes overused with multiple conditions or multiple counters, but this is really bad and not readable. We should use while in this case.

Do you know what all mentioned languages have in common? They were designed more than 25 years ago. Now let’s look at modern languages like: Kotlin, Rust, Swift, Typescript, Dart, Go or a little older like Scala and Groovy. Correct me if I’m wrong, but from that list only Dart, Typescript and Go have classic for. I believe Python and Ruby also doesn’t have them.

So your “across all other languages” is really very far from truth :slight_smile:

6 Likes

Seems like you are trying to start a argument here. I really don’t know why not just allow programmer to choose their way to code. Which a traditional for structure is fairly reasonable imo.

Umm… so, many modern languages actually have the traditional for. If I rephrase it to “Many modern languages do include the traditional for loop let alone a language that claim for Compatible with the Java ecosystem”. Will that be better?

Yes, many modern languages have the classic for. I didn’t cherry pick, I just checked random langs that came to my mind.

And with the assumption that some languages have it and others do not, I don’t understand what is the point in asking, why one of them specifically doesn’t. Kotlin authors intentionally decided to not include some features that they considered to not fit the language well. Other examples are static members or distinct boxed and unboxed primitives.

3 Likes

If I don’t understand something, then I will seek for an answer. I don’t think it is good to just assume everything is perfect as the way it is. As I said, I think it is a fairly reasonable question to ask, and I would like an answer from the team.

I apologize for maybe being a little rude :slight_smile: You started the discussion with two assumptions: that the classic for is just fine and it doesn’t need to be fixed and that it is a kind of standard to include it in programming languages. I think both of these assumptions are incorrect, so it is hard to discuss without first pointing this out.

But as you said, I didn’t take any part in designing Kotlin, so I can only guess why for was not added. It would be nice to have a comment from the Kotlin team.

2 Likes

First, a nitpick: Kotlin can’t keep the old-style for () loop, because it never had it in the first place! So this question is really a proposal to add it to the language. But is there sufficient reason to do so? Not that I can see.

When it was introduced, the old-style for () loop had one major advantage: it was comprehensive. Its flexibility allowed you to write any type of loop: loops that iterated a fixed number of times; loops that iterated until a condition was met; loops that increment a counter, or decrement it, or follow pointer chains, or anything, or nothing; you could even control multiple loop variables with it. So C didn’t need separate language constructs for all these different types of loop: the single for () does them all.

(However, note that C also has while () and do…while () loops, even though the for () loop can cover those cases too. So the author clearly felt there was a need for some special cases, even then…)

However, the price for that flexibility is complexity, verbosity, and difficulty of reading and maintaining. For a common fixed-count iteration, you need to specify the loop variable three times, which discourages long and meaningful names; the order of clauses doesn’t match the usual thought process (putting the increment after the upper limit); it’s very easy to get the termination condition wrong (<= vs <); and other mistakes are also surprisingly easy to make and hard to spot (e.g. testing or updating the wrong variable), especially when counting down instead of up.

C’s for loop also works well with a couple of its other ultra-concise features: implied tests for zero/null, and assignments within expressions — features which are neat but error-prone, and don’t scale well to larger programs. For safety, languages such as Kotlin deliberately omit those features, reducing the benefit of the old-style for.

And in practice, in Kotlin the majority of loops iterate through the elements of some structure (list, or array, or similar); they don’t need all that complexity. (When Java introduced its ‘enhanced’ for-each loop, I found that nearly all of our loops could be replaced by it — and were simpler, clearer, and safer for doing so. Even fixed-size iterations were often a disguised way of iterating through some structure. Very few old-style for () loops remained — and they tended to be the awkward ones that were probably ripe for rewriting anyway.)

So in C (and Java ≤4), most loops are more complicated and awkward than they need to be, simply to cope with the rare cases where that complexity is needed.

With the benefit of hindsight, Kotlin makes the common case very simple, short, easy to read, and almost impossible to get wrong.

It also covers many of the remaining cases with extension methods (e.g. Reader.useLines() avoids the need to read a line at a time until it’s null (and also takes care of closing the reader).

In my experience, that covers the vast majority of loops. And for the few which remain, you can always use while () or do…while (), which can do anything that the old-style for () could. They can’t do it all in one line, but that’s arguably a good thing for readability! The only significant disadvantage is that you can’t restrict the loop variable(s) to the loop’s scope, but that’s not a major issue — and in some cases you want to check it after the loop has finished anyway.

So why would Kotlin benefit from adding the old-style for loop? Every case that would use it is already catered for — mostly in ways that are far better.

Even the supposed benefit of familiarity is reducing year by year, as C and the languages it inspired fade, or are updated to include modern-style loops too. (I’m of a generation that grew up with C — it was my main professional language for several years — and then I used Java for several more years before it gained the enhanced for-each loop, so I have a lot of experience with the old-style for. But I found the new-style for-each loop trivially easy to learn, and very natural to use.

So I see nothing that comes anywhere near to outweighing the default -100 points for a new language feature. Kotlin is a great language, not just for what it includes, but for what it omits. Any new feature must ‘pull its weight’, bringing sufficient benefits to justify the extra complexity of implementing it (and supporting it for ever more), of learning how to use it, of understanding it in other people’s code, of avoiding new types of bug, and even of answering questions about it on sites such as this one when people have trouble learning or using it…

8 Likes

I see many points that you state is the cons of using for loop in the code, instead of the cons of adding in the language. If for loop has its use and the cost of adding it is acceptable (in this case I think it is minimal), then i think it is better to add it.

Furthermore, correct me if I am wrong a tradition for loop does have differences in performance compare to for/in loop

Personally I don’t like reading all those keywords downTo or until and the damn missing until reverse. The syntax uses in for loop looks very easy to read and understand.

The last paragraph of @gidds’s answer covered this. In programming languages often less is more. We shouldn’t ask ourselves why to not add a feature, but rather why to add it. My impression is that Kotlin was designed with such approach.

Both here and in the YouTrack whenever someone requests a new feature, Kotlin team first ask why do we need it, what are typical use cases and why existing features are not sufficient for these cases.

Not necessarily. At least in the case of Kotlin/JVM most common cases for traditional for like: iterating over an int range or iterating over collection indices; are optimized into a traditional for with counter.

5 Likes

I didn’t mention the pro of adding traditional for because me and @gidds already mention in the post.

I learned programming with the old-style for-loop and used it extensively for decades.
However, for the cases described by madmax1028, I absolutely prefer the Kotlin for-loop. They are concise, easy to read and very simple to understand for anyone new to the language (independently whether coming from Java or not having programmed before).
Also, these cases cover most of the loop uses (probably 95%).

I would like to know the Kotlin way for two more cases that I recently encountered:

  1. The reverse loop.
old-style:
for (int i = array.length - 1; i >= 0; --i) {
    val element = array[i]
    ...
}

Is there something like the following?

for (element in array.reversed()) { ... }
  1. The iteration with a step different from 1.
old-style: for (int i = 0; i < n; i += 2) {}

Is there a step-keyword or something to convert a range into the required collection? e.g.

for (i in 0..n step 2) {}

Live long and prosper,
tlin47

1 Like

Did you try to compile and run your “is there something like” examples? You just provided a working Kotlin code :smiley: Although, array.reversed() creates a copy. To avoid copying you need to use asReversed() instead (or asList().asReversed() if you start with an array).

4 Likes

Well, I didn’t try to compile it today. I thought that I tried some days ago when I encountered it, but am not sure about it anymore - but I also updated to a new version recently. However, looks like Kotlin is even more intuitive than I expected. :+1:

Although now I am glad I didn’t just try to compile it, since this way you gave me the important information to use asReversed() for efficiency.

Thanks a lot.

2 Likes

You have just brought up the greatest example of why the Kotlin way is problematic, I literally forget that exist.

There is very difficult to alternate for/in loop unless you know the the correct method. As many programmers have experienced with traditional for loop in other language, you know how to alternate the parameter of a for loop to get the desired result.

While, in for/in loop you force to use a existing method to get the result. Like how in hell am I suppose to know a asReversed() let alone using it properly. Am I suppose to ask the community or look for couple of documents every time when I try to alternate my for/in loop?

A for loop is like a line and a for/in loop is like a rectangle. While rectangle can be very useful in many ways, sometime you just want the flexibility of a line in certain situations.

It sounds like your concern is about making coding in Kotlin more convenient for you personally. But having multiple ways to do things makes it harder to collaborate and work as a team.

I want to second this sentiment:

In sounds like the looping constructs you’re using for are edge-cases. You may use them frequently, but as far as aggregate use in Kotlin, they’re rare. Given this, as a collaborator, I’d want to see your code be very explicit about the way it’s iterating, and descriptive functions like step or asReversed() go a long way towards explaining what your code actually does. To my mind, it’s better that you spend the extra time to learn the proper Kotlin constructs for this than for everyone you collaborate with to have to spend extra time parsing a cryptic for loop.

2 Likes

Maybe use a decent IDE, write “.rev” and see what you get? :wink:

But being serious: yes, you are correct. Foreach, iterators, ranges and stuff require additional knowledge. For is simple, works for almost all cases and you always use it in the same way. But don’t you think exactly the same argument could be used… against for? We don’t really need it after all, we can do everything with simple if and goto. All these fors, foreaches, whiles, do-whiles, continues, breaks seems pretty complicated if compared to just two commands. And each of these statements is itself more complicated than a very simple goto.

The thing is: by having all this stuff the code is more concise, less error-prone and it focuses more on what to do and not how to do it. But it requires additional knowledge and skills, this is true. And exactly the same can be said about foreach vs classic for. Or about functional/declarative style of collection processing vs simple loop - we can transform everything with loops, but functional style is much more readable. Assuming you first took some time to learn it.

Of course, you don’t suggest to remove ranges, but rather to include classic for. But my point is: this pattern of simpler low-level instructions and more complicated high-level instructions that produce better code is pretty common. And we decided to not include goto in most languages, even if maybe some people would like to use it. I think the same thing happens to for in many languages.

Also, even if we have a classic for statement, you can’t really avoid learning stdlib and how to use collections. Let’s say in your project you need to iterate over a collection, so you use foreach. A day later you need to iterate over another collection, but in the reverse order. You don’t know how to do it with foreach, so you decide to use classic for instead. Then another day you have to reverse a collection, so you look how to do it, you find reversed() function and you use it. Then in three places in your code you do very similar things in a totally different way. This isn’t optimal for code clarity. In Kotlin you would approach all three cases in a consistent way. This is exactly why I said less is often more.

4 Likes

Whatever bro, there is no point on discussing if the devs have no intention on commenting or reviewing this.

Maybe this is tangental, but you can implement the classic for loop, or something close to it at least, in Kotlin today
TL;DR: Moral of the story is that Kotlin is a powerful enough language with DSL capabilities like extension receivers and inline functions which ultimately mean that you can effectively design your own efficnet control flow statements with enough fiddling. IMHO the classic for-loop is, for this reason and the others mentioned before, not worth adding.
For the interested, however, here’s a classic for-loop in Kotlin!:


fun main() {
    For(1, { it < 10 }, { it + 1 }) { i ->
        print(i)
    }
    println()
    For(0, { it < 10 }, { it + 2 }) { i ->
        print(i)
    }
    println()
    val array = charArrayOf('o', 'n', '\n', 'o', 'l', 'l', 'e', 'h')
    For(array.size - 1, { it >= 0 }, { it - 1 }) { i ->
        print(array[i])
        // It's meant to read like English
        if (array[i] == '\n') return@For andBreak() 
        // Sadly break alone can't be supported unless you use exceptions
        // Which aren't the best for performance. You'll find an implementation
        // that uses exceptions at the end of this post.
    }
    // However, continue is just return@For, and you can name nested For blocks like so:
    For('a', { it < 'z' }, { it + 1 }) myLoop@{ c ->
        if (c == 's') return@myLoop // continue
        print(c)
    }
}

class BreakScope {
    var isBroken: Boolean = false
    	private set
    
    fun andBreak(): Unit { isBroken = true }
}

inline fun <T> For(init: T, condition: (T) -> Boolean, increment: (T) -> T, body: BreakScope.(T) -> Unit) {
    val scope = BreakScope()
    var counter = init
    while(condition(counter)) {
        scope.body(counter)
        if(scope.isBroken) break
        counter = increment(counter)
    }
}

Yes this uses an object allocation to support breaking, however, with copy funs (a feature that’s currently being designed for value classes), that allocation would disappear. It’d look something like this:

@JvmInline value class BreakScope(private copy var _isBroken: Boolean) {
    val isBroken: Boolean get() = _isBroken
    
    copy fun andBreak() { _isBroken = true }
}

inline fun <T> For(init: T, condition: (T) -> Boolean, increment: (T) -> T, body: BreakScope.(T) -> Unit) {
    val scope = BreakScope() // This will be a plain ol' java bool at runtime
    var counter = init
    while(condition(counter)) {
        scope.body(counter)
        if(scope.isBroken) break
        counter = increment(counter)
    }
}

You can also define a For loop that doesn’t force you to have one variable (even though that’s the most common use case). Only downside is that you’ll have to define your variables before the loop (i.e.:

var myVar: Int = 0
For({ myVar = 5 }, { myVar <= 100 }, { myVar *= 2 }) { /*body*/ }
// this is with a For function that looks like this:
inline fun <T> For(init: () -> Unit, condition: () -> Boolean, increment: () -> Unit, body: BreakScope.() -> Unit)  { /*implementation is trivial*/ }

Finally, here’s a variation on the very first example that breaks using an optimized exception:

private object BreakThrowable: Throwable() {
    override fun fillInStackTrace(): Throwable = this // Good for performance
}
class BreakScope {
    var isBroken: Boolean = false
    	private set
    
    fun Break(): Nothing { 
        isBroken = true 
        throw BreakThrowable
    }
}

inline fun <T> For(init: T, condition: (T) -> Boolean, increment: (T) -> T, body: BreakScope.(T) -> Unit) {
    val scope = BreakScope()
    var counter = init
    while(condition(counter)) {
        try {
        	scope.body(counter)
        } catch(e: BreakThrowable) {
            if(scope.isBroken) // This is to support nested Fors, only the For that got cancelled will actually break
                break
            else
                throw e
        }
        counter = increment(counter)
    }
}

fun main() {
    For(0, { it < 15 }, { it + 1 }) outer@{ i ->
        For(1, { it < 256 }, { it * 2 }) inner@{ j -> 
            println("$i, $j")
            if((i + j) % 2 == 0) this@inner.Break() // should print only i+1 when i is odd and then increment i, when i is even, it should print j=1 and j=2
            if(i * j == 12) this@outer.Break() // should break at i=12 and j=1 since that's the first i+j that is odd which happen to multiply to 12
        }
    }
}

Same story here, one allocation which will disappear when copy funs are a thing in Kotlin. If you don’t even need breaking, then this gets even simpler.

2 Likes