Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

When are steps of size zero allowed in Ranges? #15218

Open
gajomi opened this issue Feb 24, 2016 · 10 comments
Open

When are steps of size zero allowed in Ranges? #15218

gajomi opened this issue Feb 24, 2016 · 10 comments
Labels
design Design of APIs or of the language itself

Comments

@gajomi
Copy link
Contributor

gajomi commented Feb 24, 2016

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.

@gajomi
Copy link
Contributor Author

gajomi commented Feb 24, 2016

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

@StefanKarpinski
Copy link
Member

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.

@DNF2
Copy link

DNF2 commented Feb 24, 2016

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

@eschnett
Copy link
Contributor

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.

@eschnett
Copy link
Contributor

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.

@gajomi
Copy link
Contributor Author

gajomi commented Feb 24, 2016

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).

@vtjnash vtjnash added the needs decision A decision on this change is needed label Mar 8, 2016
@StefanKarpinski StefanKarpinski added design Design of APIs or of the language itself and removed needs decision A decision on this change is needed labels Sep 13, 2016
@StefanKarpinski StefanKarpinski added this to the 0.6.0 milestone Sep 13, 2016
@StefanKarpinski StefanKarpinski modified the milestones: 1.0, 0.6.0 Dec 15, 2016
@StefanKarpinski
Copy link
Member

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

@StefanKarpinski
Copy link
Member

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.

@StefanKarpinski StefanKarpinski modified the milestones: 1.x, 1.0 Jul 27, 2017
@timholy
Copy link
Member

timholy commented Jul 27, 2017

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.

@StefanKarpinski
Copy link
Member

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
design Design of APIs or of the language itself
Projects
None yet
Development

No branches or pull requests

7 participants