Julia: When are steps of size zero allowed in Ranges?

Created on 24 Feb 2016  路  10Comments  路  Source: JuliaLang/julia

At the moment some Range constructors disallow steps of size zero:

julia> 1:0:1
ERROR: ArgumentError: step cannot be zero
 in steprange_last(::Int64, ::Int64, ::Int64) at ./range.jl:30
 [inlined code] from ./range.jl:20
 in colon(::Int64, ::Int64, ::Int64) at ./range.jl:97
 in eval(::Module, ::Any) at ./boot.jl:267

In other cases it seems to be allowed:

julia> range(1.,0.,2)
2-element FloatRange{Float64}:
 1.0,1.0
julia> linspace(1.,1.,2)
2-element LinSpace{Float64}:
 1.0,1.0

Are the last two examples to be considered bugs or are there some cases in which steps of size zero are considered valid? Whichever the case, there seems to be a need to document the correct behavior both for devs and users.

design

Most helpful comment

Sorry I hadn't even noticed the earlier discussion. As usual, you've thought more broadly and discovered a reason that argues in favor of something I would have naively said we should disallow.

I don't think I put any effort into thinking about zero-step ranges, so if we allow them someone should go through the code and audit it. Addressing some of the specific questions:

  • > Should we allow 1:0:1 to create a 1-element range?

I say yes.

  • Are the implementation details of types in Base part of the API stability commitment of 1.0?

    If not, we need to be very clear that first, last, and step constitute the official interface and that nothing else is supported. I've seen lots of code (including some in Base, including some I wrote) that directly accessed fields of the Ranges. (Personally I think no matter what we decide about API stability, it's a good idea to push people to support the function-based interface.)

  • We should probably review the names of all the range types since some of them are a bit weird, partly due to needing to change them for deprecations.

    Sounds good. There have been (welcome) "threats" to consider unifying LinSpace, StepRange, and StepRangeLen, but AFAIK there isn't yet a PR.

All 10 comments

I would like to argue that steps of size zero should always be disallowed (and would require methods specialized to numeric-like types to check for this). I have definitely shot myself in the foot many times constructing steps of size zero in the past when home rolling my own ranges in lower level languages. For the applications I am familiar with there is always a tight coupling between the length of a range and the displacement between the two endpoints. Sometimes it makes sense to have a range with one element, but usually these have to be special cased.

EDIT: spelling

I'm actually in favor of allowing zero steps everywhere for the nice arithmetic closure properties that allows. We let people do r1 - r2 but currently for some range types this is an error, while for others it isn't:

julia> (1:10) - (0:9)
ERROR: ArgumentError: step cannot be zero
 in steprange_last(::Int64, ::Int64, ::Int64) at ./range.jl:30
 [inlined code] from ./range.jl:20
 in -(::UnitRange{Int64}, ::UnitRange{Int64}) at ./operators.jl:413
 in eval(::Module, ::Any) at ./boot.jl:260

julia> (1:1:10) - (0:1:9)
ERROR: ArgumentError: step cannot be zero
 in steprange_last(::Int64, ::Int64, ::Int64) at ./range.jl:30
 [inlined code] from ./range.jl:20
 in -(::StepRange{Int64,Int64}, ::StepRange{Int64,Int64}) at ./operators.jl:413
 in eval(::Module, ::Any) at ./boot.jl:260

julia> (1.0:10) - (0.0:9)
10-element FloatRange{Float64}:
 1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0

That's kind of annoying and harmful to generic programming. There's also the potential in the future that broadcasting could be simply and elegantly implemented by slicing arrays with zero-step ranges.

What about ranges with _almost zero_ steps? They seem a bit odd.

julia> r = linspace(0.0, nextfloat(0.0), 3)
linspace(0.0,5.0e-324,3)

julia> length(r)
3

julia> length(unique(r))
2

Subtracting two ranges makes sense; of course, in this case, one cannot calculate the length by dividing by the step size. Thus the internal representation needs to change to allow this -- we need start, step, length.

The case for integer ranges and floating-point ranges is different since floating-point ranges have to handle rounding.

Having said this, defining a floating-point range via start, stop, and (integer) length probably makes more sense than start, stop, and `step.

I hadn't thought about doing arithmetic with ranges, but it seems like a good reason to allow steps of size zero.

There is a question though of the exceptional case of UnitRange regarding the appropriate return type for things like

(1:10) - (0:9)

Would this return a StepRange? To me the name UnitRange implies that the step is of size one (in the extant implementation the only fields are start and stop with the length is inferred from the same).

Any thoughts on this? @timholy, does the new range work you did happen to allow this generally?

I don't think we're going to take away the current ability to have zero-step float ranges, and if we're going to change this in any direction, it would be to allow zero-step ranges for more range types, not fewer, which makes changing this a non-breaking change.

Other issues that came up during discussion:

  • Should we allow 1:0:1 to create a 1-element range? There's a continuity argument that 1:x:1 is a 1-element range for any value of x besides 0.

  • Are the implementation details of types in Base part of the API stability commitment of 1.0? If so, that's a fairly strict requirement.

  • We should probably review the names of all the range types since some of them are a bit weird, partly due to needing to change them for deprecations.

Sorry I hadn't even noticed the earlier discussion. As usual, you've thought more broadly and discovered a reason that argues in favor of something I would have naively said we should disallow.

I don't think I put any effort into thinking about zero-step ranges, so if we allow them someone should go through the code and audit it. Addressing some of the specific questions:

  • > Should we allow 1:0:1 to create a 1-element range?

I say yes.

  • Are the implementation details of types in Base part of the API stability commitment of 1.0?

    If not, we need to be very clear that first, last, and step constitute the official interface and that nothing else is supported. I've seen lots of code (including some in Base, including some I wrote) that directly accessed fields of the Ranges. (Personally I think no matter what we decide about API stability, it's a good idea to push people to support the function-based interface.)

  • We should probably review the names of all the range types since some of them are a bit weird, partly due to needing to change them for deprecations.

    Sounds good. There have been (welcome) "threats" to consider unifying LinSpace, StepRange, and StepRangeLen, but AFAIK there isn't yet a PR.

Oh, one more thing: I should point out that allowing zero-step ranges to be created and having a syntax for them is completely independent. While 1:0:1 could be allowed to create a 1-element, zero-step range, 1:0:2 would still presumably not work since it would have infinite length. So the only way to get zero-step ranges of length other than one, would be via range arithmetic or calling constructors where one provides a length explicitly.

Was this page helpful?
0 / 5 - 0 ratings

Related issues

manor picture manor  路  3Comments

helgee picture helgee  路  3Comments

yurivish picture yurivish  路  3Comments

wilburtownsend picture wilburtownsend  路  3Comments

felixrehren picture felixrehren  路  3Comments