Ranges Reloaded

In Kotlin M5 we have redesigned our ranges a little bit.

Range expressions are formed with rangeTo functions that have the operator form of .. which are complemented by in and !in. Range is defined for any comparable type (subclass of Comparable), but for number primitives it is optimized. Here are examples of using ranges:

Numerical ranges have extra feature: they can be iterated over. Compiler takes care about converting this in simple analogue of Java’s indexed for-loop, without extra overhead. Examples:

What if you want to iterate over numbers in reversed order? It’s simple. You can use downTo() function defined in standard library:

Is it possible to iterate over numbers with arbitrary step, not equal to 1? Sure, step() function will help you:

How it works

There are two traits in the library: Range<T> and Progression<N>.

Range<T> denotes an interval in the mathematical sense, defined for comparable types. It has two endpoints: start and end, which are included in the range. Main operation is contains(), usually used in the form of in/!in operators.

Progression<N> denotes arithmetic progression, defined for number types. It has startend and non-zero incrementProgression<N> is a subtype of Iterable<N>, so it can be used in for-loops and functions like mapfilter, etc. First element is start, every next element equals previous plus increment. Iteration over Progression is equivalent to an indexed for-loop in Java/JavaScript:

For numbers, the “..” operator creates an object which is both Range and Progression. Result of downTo() and step() functions is always a Progression.

About Evgeny Gerashchenko

Evgeny is the developer working on Project Kotlin at JetBrains.
This entry was posted in General, Language design and tagged . Bookmark the permalink.

11 Responses to Ranges Reloaded

  1. skin27 says:

    the downTo function is practical, but why does the .. operator not first check if left is greater of smaller than the right and call a upTo() or downTo() after that. If someone first tries the .. operator he will be surprised that ” for (i in 4..1) print(i) ” prints nothing. Or will he be guided by the IDE to use the downTo function?

    • Evgeny Gerashchenko says:

      Because if we do so, it will be the big source of bugs: when you expect your program to not iterate at all, it will iterate in reversed direction. There are different syntax for direct and reversed iteration, you always know what you mean and what you expect.

      Let me show you simple example. Consider that “..” automatically iterates backwards, when first end is greater than second.

      fun f(a: List) {
          for (i in 0..a.size() - 1) {
              println(a[i])
          }
      }

      This function works perfectly for non-empty lists. But what if the list is empty? a.size() == 0. First, code will try to access element number 0. If we somehow escape IndexOutOfBoundsException, it will try to access element number −1 on next iteration. That’s not what you want to expect.

      IDE inspection for loops which never iterate is a good idea, I have added a request to issue tracker: KT-3333.

      • Stephen Colebourne says:

        Perhaps you should allow 4..1 if both ends of the range are literals.

        • Evgeny Gerashchenko says:

          That would be even more error-prone: 4..1 would mean absolutely different from 2+2..1

          • AssD says:

            Haskell’s doing fine with it. You might get some edge cases, but it shouldn’t be a rocket science to cover them.

          • skin27 says:

            Thnx, for your explanation. I can now imagine that are several cases where this can lead to errors.

            In the array example though I would expect an error, so I would know I am iterating an empty list. Now there is no clear way to find this out.

            On the subject of array. I personally find it always a bit confusing that array indexes start from 0, instead of 1 (so that maxIndex is size – 1, instead of maxIndex equals arraysize). Maybe there could be IDE inspection on this one too (common mistake to forget the -1)?

      • Daniel Siegmann says:

        If I write 4..1 my expectation is it will iterate from 4 down to 1. In fact I ran into this when learning Groovy: I found a case where I needed this reverse iteration, and tried that approach hoping it would work. It did.

        If 4..1 doesn’t provide reverse iteration, it should be throwing IllegalArgumentException or something similar. I would certainly not expect it to do nothing – to me that is a silent failure.

        The code example you provide is obviously buggy. You need to check whether the list is empty – as a Java developer, that would be immediately obvious to me. A better solution is to allow an exclusive boundary, as Groovy does; e.g. 0..<a.size().

        Of course, in Kotlin I would expect to write something like a.each { ... }

  2. Romeo Frenksen says:

    Hi,

    you stated, that

    for (i in 4..1) print(i) // prints nothing

    won’t print anything, but the following should be used instead:

    for (i in 4 downTo 1) print(i) // prints "4321"

    To me, it’d be more intuitive, if the first option would be possible, too. Furthermore, the second variant just adds complexity to the language as you have to memorize an additional word. Could you please explain the pro/cons for the current implementation?

    Thanks.

  3. Marc-Olivier Fleury says:

    Why not use upTo instead of just .. for progressions? Although more verbose, this version makes it clear that the first bound has to be lower.

    Additionally, I find it a bit awkward to use the same construct for both progression and range. It seems to be a a cool feafture when first looking at it, but the different remarks posted here are probably an indicator that something is wrong.

    An example of an awkward case: using a progression with real numers requires a step, but not with integers. However, ranges will never require steps, even for real numbers.

    Bound ordering is also not a problem when defining ranges, but is suddenly a problem when using progressions.

    These are a few reasons why I would limit the use of .. for ranges, and require upTo or downTo for progressions.

  4. Anton says:

    In 95% of cases we will have an iteration over indexed collection:
    for ( i in 0..nodes.length()-1 ) { val node = nodes.get(i); ... }
    Some languages permit an exclusion construction like:
    for ( i in 0..<nodes.length() ) { val node = nodes.get(i); ... }
    which is especially useful in the iteration as above.

Leave a Reply

Your email address will not be published. Required fields are marked *

You may use these HTML tags and attributes: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code class="" title="" data-url=""> <del datetime=""> <em> <i> <q cite=""> <strike> <strong> <pre class="" title="" data-url=""> <span class="" title="" data-url="">