The colon punctuation: Why doesn't 5:1 return [5, 4, 3, 2, 1]?

I got surprised by this:

julia> 1:5 # Expected
1:5

julia> 5:1 # Unexpected
5:4

julia> collect(1:5) # Expected
5-element Vector{Int64}:
 1
 2
 3
 4
 5

julia> collect(5:1) # Doubly Unexpected
Int64[]

julia> collect(5:-1:1) # Ok, I *think* I get it...
5-element Vector{Int64}:
 5
 4
 3
 2
 1

The documentation says:

a:b colons (:) used as a binary infix operator construct a range from a to b (inclusive) with fixed step size 1
Punctuation · The Julia Language

I expected the second output to be 5:1, ie [5, 4, 3, 2, 1]. Ok, I can see that reading carefully the stepsize is 1, when I wanted -1. But why does it say 5:4? And why not have it automatically switch the sign if a > b?

Has adding a note about this to the docs been considered?

By the same token, if 5:4 meant 5:-1:4 as you suggest, then it would be impossible to express empty ranges.

7 Likes

You pretty much answered your own question on why it’s not equal to [5,4,3,2,1]: it’s because that’s not its definition. Yes, that’s tautological. :slight_smile: Steven has a good motivation for the current behavior, but a different choice could have been made.

Julia “normalizes” empty ranges such that the end is one step less than the beginning, so that’s why it’s 5:4 and not 5:1. Why? IIRC it helps with some efficiencies for all ranges, even non-empty ones, if you can assume that.

Could docs be improved? Always!

8 Likes

Take a look at the function range and the rules become clearer. If you specify only start and stop the step is assumed to be 1.

help?> range
search: range LinRange UnitRange StepRange StepRangeLen

  range(start, stop, length)
  range(start, stop; length, step)
  range(start; length, stop, step)
  range(;start, length, stop, step)

  Construct a specialized array with evenly spaced elements and
  optimized storage (an AbstractRange) from the arguments.
  Mathematically a range is uniquely determined by any three of
  start, step, stop and length. Valid invocations of range are:

    •  Call range with any three of start, step, stop,
       length.

    •  Call range with two of start, stop, length. In this
       case step will be assumed to be one. If both
       arguments are Integers, a UnitRange will be
       returned.

    •  Call range with one of stop or length. start and
       step will be assumed to be one.

With the colon syntax, you can specify start:step:stop.

1 Like

An empty range could be expressed as 5:1:4 (i.e. by explicitly specifying a positive step size)

I think there are a few different points to this topic.

  • Definition/characteristics of ranges (start, length, stop, step)
  • Why are empty ranges “normalised”
  • What are default values for under-specified ranges, in particular using colon syntax.

In some contexts, I think it would be reasonable to expect differing defaults for when a range is under-specified. When start and stop are explicitly specified with literals (e.g. 3:7, 5:1), I don’t think it is unreasonable to expect a default step size of +/-1 to make a non-empty range.

However, personally I do like the current default step size of +1 (allowing empty ranges), because it simplifies the following type of logic of iterating through 0 or more items:

# loop is skipped if n <= 0
for i in 1:n
    ...
end

No, because then m:step:n would not be type stable. Julia uses a separate UnitRange type to represent m:n ranges (in which the step size is implicitly 1), in order to save 1/3 of the storage in this common case and to enable other optimizations.

julia> typeof(5:4)
UnitRange{Int64}

julia> typeof(5:1:4)
StepRange{Int64, Int64}

Similarly, automatically converting m:n to n:-1:m when n < m would not be type stable.

2 Likes

Oh, good point.
Thanks for pointing this out!

Perhaps we should make start:Val(step):stop work.