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:
if (i in 1..10) { // equivalent of 1 <= i && i <= 10
println(i)
}
if (x !in 1.0..3.0) println(x)
if (str in "island".."isle") println(str)
// equivalent of "island" <= str && str <= "isle"
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:
for (i in 1..4) print(i) // prints "1234"
for (i in 4..1) print(i) // prints nothing
for (x in 1.0..2.0) print("$x ") // prints "1.0 2.0 "
What if you want to iterate over numbers in reversed order? It’s simple. You can use downTo() function defined in standard library:
for (i in 4 downTo 1) print(i) // prints "4321"
Is it possible to iterate over numbers with arbitrary step, not equal to 1? Sure, step() function will help you:
for (i in 1..4 step 2) print(i) // prints "13"
for (i in 4 downTo 1 step 2) print(i) // prints "42"
for (i in 1.0..2.0 step 0.3) print("$x ") // prints "1.0 1.3 1.6 1.9 "
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 start, end and non-zero increment. Progression<N> is a subtype of Iterable<N>, so it can be used in for-loops and functions like map, filter, 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:
// if increment > 0
for (int i = start; i <= end; i += increment) {
// ...
}
// if increment < 0
for (int i = start; i >= end; i += increment) {
// ...
}
For numbers, the “..” operator creates an object which is both Range and Progression. Result of downTo() and step() functions is always a Progression.
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?
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 escapeIndexOutOfBoundsException, 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.
Perhaps you should allow 4..1 if both ends of the range are literals.
That would be even more error-prone: 4..1 would mean absolutely different from 2+2..1
Haskell’s doing fine with it. You might get some edge cases, but it shouldn’t be a rocket science to cover them.
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)?
If I write
4..1my 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..1doesn’t provide reverse iteration, it should be throwingIllegalArgumentExceptionor 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 { ... }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.
Please read my answer to previous comment.
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.
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.