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

range(start, stop, length) #38750

Closed
antoine-levitt opened this issue Dec 7, 2020 · 60 comments · Fixed by #39228
Closed

range(start, stop, length) #38750

antoine-levitt opened this issue Dec 7, 2020 · 60 comments · Fixed by #39228
Labels
design Design of APIs or of the language itself

Comments

@antoine-levitt
Copy link
Contributor

antoine-levitt commented Dec 7, 2020

This issue is to propose defining range(start, stop, length) = range(start, stop; length=length). I searched the issues and PR, expecting pages of heated debate, but I couldn't find any, so here goes.

Pros:

  • It's really really useful syntax. Doing grep -r ' range(' in my .julia/packages returns lots of hits, almost all of which are range(start, stop; length=length). Doing the same in my research codes has a lot more, all of them of this form. Most of my usage is either discretization of a differential equation or plotting of a function.
  • Having to type length is very annoying. I think this is the reason why some people use LinRange, which increases fragmentation and makes people use LinRange when they probably shouldn't (it's low-level compared to range)
  • This definition doesn't break anything
  • step already has its own nice syntax (a:b:c), length is missing one

Cons:

  • Range has a large ambiguity between the different ways of specifying it, and this would force a default choice.
  • It could plausibly be thought that range(a, b, c) and a:b:c do the same thing. I don't think this is a serious problem since the a:b:c syntax is clearly special, and not analogous to function calls (since the additional argument comes in the middle)
  • range(a, b, c) and LinRange(a, b, c) would not be similar syntax for different things
  • It's clear at first sight what range(0, 1, length=100) does; range(0, 1, 100) is more implicit and can plausibly cause confusion. I think it's usually clear from the context. In most of examples taken from my usage, the length keyword was called N or something explicit like that.
  • It's not very nice to have the same argument as both positional and keyword.
@JeffBezanson JeffBezanson added the design Design of APIs or of the language itself label Dec 7, 2020
@timholy
Copy link
Sponsor Member

timholy commented Dec 7, 2020

xref #38041 (which is an open pull request)

@antoine-levitt
Copy link
Contributor Author

Related but orthogonal: the proposed three-arg version range(a, b, c) would not take any kwarg

@mkitti
Copy link
Contributor

mkitti commented Dec 7, 2020

xref #37875 which is an open documentation only pull request

@johnnychen94
Copy link
Sponsor Member

FWIW, I believe #38041 can safely close #37875 since it makes the usage much simpler to intuitive.

@antoine-levitt
Copy link
Contributor Author

Since nobody seems to think this is an extremely bad idea, I'll make a PR once #38041 is in

@mkitti
Copy link
Contributor

mkitti commented Dec 16, 2020

What troubles me about this is the order of the arguments is not clear. I suggest a new name for this function that also communicates the order of the arguments. Keeping the current argument names I would prefer range_start_stop_length(start, stop, length) though this is a bit long.

Part of the reason for the length is that the arguments are hard to unambiguously abbreviate. start, stop, and step share the first two letters. Therefore, I propose a renaming of the arguments:

start -> begin
stop -> end

This refers to existing indexing terms:

julia> a = 1:0.2:5
1.0:0.2:5.0

julia> a[begin]
1.0

julia> a[end]
5.0

julia> length(a)
21

julia> step(a)
0.2

We could then have a group of range functions that use the first letters of begin, end, length, and step to indicate the order of the arguments:

# Three args
range_bel( begin_arg, end_arg, length ) = range( begin_arg, end_arg, length = length )
range_bes( begin_arg, end_arg, step ) = range( begin_arg, end_arg, step = step ) 
range_bls( begin_arg, length, step ) = range( begin_arg, length = length, step = step )
range_els( end_arg, length, step ) = range( stop = end_arg, length = length, step = step ) # Needs 38041 

# Two args
range_be( begin_arg, end_arg ) = range( begin_arg, stop = end_arg )
range_bl( begin_arg, length ) = range( begin_arg, length = length )
# range_bs - not clear what length or end might be
range_el( end_arg, length ) = range( stop = end_arg, length = length, step = 1 ) # Needs 38041
range_es( end_arg, step ) = range(1, stop = end_arg, step = step)
range_ls( length, step ) = range(1, length = length, step = step)

@johnnychen94
Copy link
Sponsor Member

johnnychen94 commented Dec 16, 2020

We could then have a group of range functions that use the first letters of begin, end, length, and step to indicate the order of the arguments:

In this case, I'd personally use the constructor, i.e., StepRange(args...) and it is the clearest way while still simple.

I mean, it's totally fine to write some ad-hoc helpers for this, but I don't think they should live in Base.

@mkitti
Copy link
Contributor

mkitti commented Dec 16, 2020

In this case, I'd personally use the constructor, i.e., StepRange(args...) and it is the clearest way while still simple.

That does not help this PR where the request is for range(start, stop, length). I would need to look up the order these arguments every time and would likely confuse the order of the arguments with that of StepRange(start, step, stop). In this case, would you also oppose the addition of range(start, stop, length)?

My position is that I would prefer range_bel(begin_a, end_a, length) over range(start, stop, length) if we were to add this.

@mkitti
Copy link
Contributor

mkitti commented Dec 17, 2020

One could just do one of the following in lieu of this PR.

range(a, b, l)  = Base._range(a, nothing, b, l)
range_bel(b, e, l) = Base._range(b, nothing, e, l)

#38041 implements range_start_stop_length so one could just do after that is merged.

import Base: range_start_stop_length

julia/base/range.jl

Lines 505 to 515 in 8c327e9

function range_start_stop_length(start::T, stop::S, len::Integer) where {T,S}
a, b = promote(start, stop)
range_start_stop_length(a, b, len)
end
range_start_stop_length(start::T, stop::T, len::Integer) where {T<:Real} = LinRange{T}(start, stop, len)
range_start_stop_length(start::T, stop::T, len::Integer) where {T} = LinRange{T}(start, stop, len)
range_start_stop_length(start::T, stop::T, len::Integer) where {T<:Integer} =
_linspace(float(T), start, stop, len)
## for Float16, Float32, and Float64 we hit twiceprecision.jl to lift to higher precision StepRangeLen
# for all other types we fall back to a plain old LinRange
_linspace(::Type{T}, start::Integer, stop::Integer, len::Integer) where T = LinRange{T}(start, stop, len)

Overall, I find the original proposal confusing. 😕

@antoine-levitt
Copy link
Contributor Author

antoine-levitt commented Dec 17, 2020

Let's not overestimate the utility here of all different possible variants. Range is overwhelmingly used as range(a, b, length=N):

antoine@epsilon ~/.julia/packages $ grep -ir ' range(' * | wc -l
180
antoine@epsilon ~/.julia/packages $ grep -ir ' range(' * | grep length | wc -l
165

Of the remaining usages, most are kind of artificial:

antoine@epsilon ~/.julia/packages $ grep -ir ' range(' * | grep -v length
Colors/kc2v8/src/utilities.jl:    range(start::T, stop::T; kwargs...) where T<:Colorant = range(start; stop=stop, kwargs...)
Compat/qsiOu/test/runtests.jl:@test range(0, 10, step = 2) == 0:2:10
Compat/qsiOu/test/runtests.jl:@test_throws ArgumentError range(0, 10)
Compat/qsiOu/src/Compat.jl:        range(start; stop=stop, kwargs...)
FillArrays/tE9Xq/src/FillArrays.jl:cumsum(x::AbstractFill{<:Any,1}) = range(getindex_value(x); step=getindex_value(x),
Gtk/C22jV/gen/gbox3:    function range(range_::Gtk.GtkRange, min, max)
Gtk/C22jV/gen/gbox3:    function range(spin_button::Gtk.GtkSpinButton, min, max)
Gtk/C22jV/gen/gbox3:    function range(spin_button::Gtk.GtkSpinButton)
Gtk/C22jV/gen/gbox2:    function range(range_::Gtk.GtkRange,min,max)
Gtk/C22jV/gen/gbox2:    function range(spin_button::Gtk.GtkSpinButton,min,max)
Gtk/C22jV/gen/gbox2:    function range(spin_button::Gtk.GtkSpinButton)
NCDatasets/Zat6R/test/perf/benchmark-python-netCDF4.py:    for n in range(v.shape[0]):
NCDatasets/HhdCu/test/perf/benchmark-python-netCDF4.py:    for n in range(v.shape[0]):
PlotThemes/4DCOG/src/juno_smart.jl:        append!(grad, range(colvec[i], stop = colvec[i+1]))
Unitful/1t88N/src/range.jl:# the following is needed to give sane error messages when doing e.g. range(1°, 2V, 5)

. So clearly range is mostly used to create equispaced grids. The reason is that other uses already have the a:b:c syntax.

That does not help this PR where the request is for range(start, stop, length). I would need to look up the order these arguments every time and would likely confuse the order of the arguments with that of StepRange(start, step, stop)

Do you really use StepRange directly? Regarding the possible confusion, I agree it's ambiguous but explained in my original post why I don't think this is too much of an issue. In particular, uses involving length are usually quite different from those involving step, and there's much less chance of confusion than in, say, step and stop. I think it would be pretty clear with the proposal in the OP plus #38041: the prototype is range([, start, stop, length]; start, stop, step, length). All valid combinations are accepted.

I like the idea of replacing start by begin and stop by end, but both begin and end are keywords.

@jebej
Copy link
Contributor

jebej commented Dec 17, 2020

maybe we can get linspace back :trollface:

@StefanKarpinski StefanKarpinski added the triage This should be discussed on a triage call label Dec 17, 2020
@JeffBezanson
Copy link
Sponsor Member

One downside: python has range(start, stop, step) which is very confusing.

@StefanKarpinski
Copy link
Sponsor Member

I agree that it's weird to require one keyword argument for a function like this, but Python's range and numpy.arange functions take start, stop, step positional arguments, so diverging from that seems like a bad idea. At the same time, adding range(start, stop, step) seems kind of pointless since we already have dedicated syntax for that construction.

Triage notes that range(start, stop) doesn't work despite the docs claiming that the default step is one, but it would probably be fine to make that work.

@mkitti
Copy link
Contributor

mkitti commented Dec 17, 2020

You have to give stop as a keyword:

julia> range(1, stop = 5)
1:5

@mkitti
Copy link
Contributor

mkitti commented Dec 17, 2020

My recommendation is consider #38041 which creates a Base.range_start_stop_length which implements this function exactly. We could then consider whether it would be worth aliasing and exporting that function.

@antoine-levitt
Copy link
Contributor Author

@mkitti the point of this is to make things shorter: there's no real point in having a range_start_stop_length which would not be shorter than the kwarg version.

@triage Thanks for discussing this! Oof I did not know about python's range(start, stop, step), that sucks. I would still argue the potential for confusion is very limited for the reasons given above, but I understand the reluctance to do it in this case. Then yes, possibly the least bad solution is to bring back linspace from the dead? (or call it linrange or something)

@KristofferC KristofferC modified the milestone: 1.6 blockers Dec 18, 2020
@jebej
Copy link
Contributor

jebej commented Dec 18, 2020

Remember that we already have LinRange. I think it makes sense to have linspace back to dispatch either to LinRange or the twice-precision StepRangeLength.

@DNF2
Copy link

DNF2 commented Dec 18, 2020

range(start, stop, step) seems a lot less useful than the proposed range(start, stop, length). Speaking as a Matlab user, I welcome this as a drop-in replacement for linspace. The order of arguments is so ingrained in Matlab users, at least, that it's basically hardwired.

For those who are confused about order, there would still be the keywords, right?

@mkitti
Copy link
Contributor

mkitti commented Dec 21, 2020

The implementation for the request here can be currently done in a single line of code which eventually leads to the use of LinRange

Base.range(start, stop, length) = Base._range(start, nothing, stop, length)

Base.range_start_stop_length in #38041 makes this even friendlier due to the lack of leading underscore:

Base.range(start, stop, length) = Base.range_start_stop_length(start, stop, length)

@DNF2, If you want a drop-in replacement for MATLAB's linspace couldn't you just use LinRange? It's also rather performant if you have MATLAB-level tolerance for floating point errors and using floats everywhere.

julia> LinRange(0, 20, 61)
61-element LinRange{Float64}:
 0.0,0.333333,0.666667,1.0,1.33333,1.66667,2.0,2.33333,,17.6667,18.0,18.3333,18.6667,19.0,19.3333,19.6667,20.0
 
 julia> len = typemax(Int)
9223372036854775807

julia> @benchmark LinRange(0, 10, $len)
BenchmarkTools.Trial:
  memory estimate:  0 bytes
  allocs estimate:  0
  --------------
  minimum time:     0.001 ns (0.00% GC)
  median time:      0.001 ns (0.00% GC)
  mean time:        0.027 ns (0.00% GC)
  maximum time:     0.101 ns (0.00% GC)
  --------------
  samples:          10000
  evals/sample:     1000

If you really want linspace, it's actually still there if you needed in a pinch...

julia> Base._linspace(0. , 20. , 21)
0.0:1.0:20.0

To summarize, the function proposed here is an alias for a function that currently lives in Base. The main thing in contention here is if we should call this function by a different name that is exported.

  1. The original post says LinRange is low-level, but I'm not understanding this argument after the further discussion, and I think expanding upon this would help flesh out the technical merits here. Can we clarify why LinRange does not work? Is there a specific use case where this does not work?
  2. The range call from Python makes this problematic. linspace has some nice attributes of MATLAB compatibility, but there may be a reason to be distinct from MATLAB. Are there any other aliases that could work? rangel, rangelen?

@antoine-levitt
Copy link
Contributor Author

The original post says LinRange is low-level, but I'm not understanding this argument after the further discussion, and I think expanding upon this would help flesh out the technical merits here. Can we clarify why LinRange does not work? Is there a specific use case where this does not work?

This is detailed in the docs, LinRange is less careful about floating point errors than range, hence the "low-level". Also it has not-very-nice default display, which makes it look kind of like an internal thing (which should probably get fixed if this is the "recommended" way to build range with lengths). Performance of creation is irrelevant, it's just a four-int struct. Performance of indexing/SIMDization/etc could be more interesting (although pretty unlikely to matter in practice).

Generally speaking, in julia for this kind of work you don't usually call a constructor directly, and therefore using LinRange here seems weird.

I don't actually particularly care which of StepRange or LinRange a 3-arg range would dispatch to, I just want a single standard, agreed-upon, non-kwarg, non-constructor API to create a range to plot things to reduce my cognitive overhead.

@mkitti
Copy link
Contributor

mkitti commented Dec 21, 2020

Generally speaking, in julia for this kind of work you don't usually call a constructor directly, and therefore using LinRange here seems weird.

I don't actually particularly care which of StepRange or LinRange a 3-arg range would dispatch to, I just want a single standard, agreed-upon, non-kwarg, non-constructor API to create a range to plot things to reduce my cognitive overhead.

I don't really see calling the constructor as an issue, but I can see the having to the push the shift key could be annoying. Would calling it linspace or linrange and defining them as follows work for everyone?

julia> linspace(start, stop, length) = Base._range(start, nothing, stop, length)
linspace (generic function with 1 method)

julia> linrange(start, stop, length) = Base._range(start, nothing, stop, length)
linrange (generic function with 1 method)

Would you want a one or two argument version or having a length argument of nothing be supported?

@StefanKarpinski
Copy link
Sponsor Member

Maybe defining range(start, stop, length) isn't so bad even though it diverges from Python:

  1. Giving the length is generally better than giving the step so it makes sense that this takes precedence.
  2. This is similar to the classic linspace function which was previously called linrange in Julia because it doesn't create a space, it creates a range. But the lin prefix is redundant—all ranges are linear. So just calling it range makes sense.
  3. If we make range(start, stop) default to step = 1 then at least that method will agree with Python (will, aside from ours including stop) which seems like the one that Python users mostly reach for.
  4. As originally argued here, since we already have start:step:stop as dedicated syntax, it seems a bit silly to make range(a, b, c) mean the same thing but with a slightly different ordering.

@jebej
Copy link
Contributor

jebej commented Dec 21, 2020

I think the name linspace isn't intended to mean creating a space, it's just a shortcut for "linearly-spaced vector/range":
https://www.mathworks.com/help/matlab/ref/linspace.html

@mkitti
Copy link
Contributor

mkitti commented Dec 21, 2020

Numpy also has linspace(start, stop, length): https://numpy.org/doc/stable/reference/generated/numpy.linspace.html

There length is called num with the documentation "Number of samples to generate. Default is 50. Must be non-negative."

https://github.com/numpy/numpy/blob/v1.19.0/numpy/core/function_base.py#L24

Likewise, numpy and matlab emulators have followed suit:
https://www.tensorflow.org/api_docs/python/tf/linspace
https://www.rdocumentation.org/packages/pracma/versions/1.9.9/topics/linspace

A potential issue for us is that in each of these cases length has a default value (MATLAB: 100, Numpy: 50)

@mkitti
Copy link
Contributor

mkitti commented Dec 21, 2020

3. If we make range(start, stop) default to step = 1 then at least that method will agree with Python (will, aside from ours including stop) which seems like the one that Python users mostly reach for.

@JeffBezanson commented on this before as noted in the source code:
#28708 (comment)

Also range(start, stop) in Python does not include stop:

In [16]: [i for i in range(1,5)]
Out[16]: [1, 2, 3, 4]

In [17]: [i for i in range(5)]
Out[17]: [0, 1, 2, 3, 4]

As much as I like range, I think Python has imprinted its conventions on it.

@mkitti
Copy link
Contributor

mkitti commented Dec 23, 2020

UnitRange is mentioned in the current documentation.

help?> range
search: range LinRange UnitRange StepRange StepRangeLen trailing_zeros AbstractRange trailing_ones OrdinalRange

  range(start[, stop]; length, stop, step=1)

  Given a starting value, construct a range either by length or from start to stop, optionally with a given step
  (defaults to 1, a UnitRange). One of length or stop is required. If length, stop, and step are all specified, they
  must agree.

From that you might expect a UnitRange by default or if step = 1. But that is not what you will get:

julia> range(1, 3.; step=1)
1.0:1.0:3.0

julia> typeof(range(1, 3.; step=1))
StepRangeLen{Float64,Base.TwicePrecision{Float64},Base.TwicePrecision{Float64}}

I wrote up documentation on how to create a UnitRange in the extended help of range in #37875

julia/base/range.jl

Lines 158 to 167 in 20b84f4

## `UnitRange` construction
To specify a [`UnitRange`](@ref) where `step` is 1, use one of the following
where `start`, `length`, and `stop` are all integers.
* range(start, length=length)
* range(start, stop=stop)
* `start:stop`
* `(:)(start,stop)`
Specifying a `step` of 1 explicitly, does not result in a [`UnitRange`](@ref).

@mkitti
Copy link
Contributor

mkitti commented Dec 24, 2020

@MasonProtter just had a great idea in the Zulip chat. Why don't we give start and stop together via some connected syntax. For example:

julia> Base.range(unit_range::UnitRange, length=nothing) = Base._range(unit_range.start, nothing, unit_range.stop, length)

julia> Base.range(pair::Pair, length=nothing) = Base._range(pair.first, nothing, pair.second, length)


julia> range(1:2)
1:2

julia> range(1:2, 5)
1.0:0.25:2.0

julia> range(3 => 5.6, 261)
3.0:0.01:5.6

We could even switch the order around and have both forms.

julia> Base.range(length, unit_range::UnitRange) = Base._range(unit_range.start, nothing, unit_range.stop, length)

julia> Base.range(length, pair::Pair) = Base._range(pair.first, nothing, pair.second, length)

julia> range(5, 1:2)
1.0:0.25:2.0

julia> range(1:2, 5)
1.0:0.25:2.0

julia> range(100, 0 => 2π)
0.0:0.06346651825433926:6.283185307179586

@mkitti
Copy link
Contributor

mkitti commented Dec 28, 2020

Noting that Colors.jl has a three-position range with length = 100 http://juliagraphics.github.io/Colors.jl/stable/colormapsandcolorscales/#Base.range

@antoine-levitt
Copy link
Contributor Author

As discussed in #39071, using linrange here would have a nice consistency with logrange, and not using range would allow for other features like the selection of endpoint inclusion.

@mkitti
Copy link
Contributor

mkitti commented Jan 2, 2021

It would be good to revisit the conversations in #25896 in #28708 to see where many of these matters were previously discussed including the existence of linrange(start, stop, length) and logrange and their deprecation. If something has changed since then, it would nice to point out what is different.

@JeffBezanson
Copy link
Sponsor Member

Triage is in favor of this.

@StefanKarpinski
Copy link
Sponsor Member

Triage is inclined to do this and also supports range(stop) to mean range(start = 1, stop = stop).

@mbauman mbauman removed the triage This should be discussed on a triage call label Jan 7, 2021
@mkitti
Copy link
Contributor

mkitti commented Jan 7, 2021

Triage is inclined to do this and also supports range(stop) to mean range(start = 1, stop = stop).

Does this mean no two argument then? No two positional argument version is currently defined.

Per above, I would be interested in range(start, length) or range(length, start) over range(start, stop) because colon handles the latter case well: start:stop .

@antoine-levitt
Copy link
Contributor Author

antoine-levitt commented Jan 7, 2021

I'm kinda disappointed we're not going with linrange instead, but triage hath spoken. I'll make a PR tomorrow.

It's impossible to make the 2-arg version be anything else than range(start, stop) without being inconsistent with the existing range(start, stop; kwargs). I'll make a PR with range(stop), range(start, stop) and range(start, stop, length) once the refactor PR is merged.

@StefanKarpinski
Copy link
Sponsor Member

Adding linrange in addition with similar but different behavior just seems a bit waffling. The range function already produces a linear range, so leaving it with an incomplete API and adding linrange instead so that we don't have to pick one behavior over the other seems like the design by committee solution that leaves people forced to memorize two different, incompatible behaviors instead of just learning one. We should pick a behavior for range and go with it.

I'm torn on which set of positional signatures makes more sense. One option is the "stop-oriented" design:

  • range(stop) with start = step = 1
  • range(start, stop) with step = 1
  • range(start, stop, length).

The other option is the "length-oriented" design:

  • range(length) with start = step = 1
  • range(start, length) with step = 1
  • range(start, stop, length).

Note that these only different in behavior of the middle two-argument signature. Are there any other positional schemes that have been proposed that I'm missing? The other one would be for step to be the third positional argument, but we've already rejected that since start:step:stop exists.

@antoine-levitt
Copy link
Contributor Author

Yeah, I just liked the consistency linrange/logrange and the possibility to add more kwargs to linrange. It's a bit orthogonal anyway in the sense that we can still do range(start, stop, length) and add linrange later if we want (although the point of this would be diminished, of course).

The most consistent version would be to follow the principles "three arguments, or two non-step, in which case step is 1" plus the order (start, stop, length) to the end. This would mean forbidding one positional argument (since it doesn't match the rule) and have range(start, stop) (which we can't really change for compatibility with the current range(start, stop; kwargs) anyway). Forbidding range(stop) would also prevent python people from tripping themselves with range(stop) (and, let's face it, it's not particularly useful)

@mbauman
Copy link
Sponsor Member

mbauman commented Jan 7, 2021

Forbidding range(stop) would also prevent python people from tripping themselves with range(stop)

I see a Julian range(3) == 1:3 as being the appropriate conversion from the python range(3) == [0,1,2] (ok, that's python 2 but you get the point). Perhaps take these one at a time — first three arg (start, stop, length) which is highly desired and agreed upon, then separately the 1-arg (stop,). Note, too, that it becomes more useful if it returns a Base.OneTo — which is commonly used in high-performance axis munging.

@mkitti
Copy link
Contributor

mkitti commented Jan 7, 2021

range(start, stop) is just start:stop like range(start, step, stop) is just start:step:stop. Adding either range(start, stop) and range(start, step, stop) just adds redundancy.

For the length oriented design, we can just document this as:

`length` is always the last argument when only positional arguments are used.

I would deprecate range(start, stop; length) by just removing the documentation for it and leaving range(start, stop; step) . I'm not sure why you would use range(start, stop; length) when you have range(start, stop, length). It could be left as a compatibility note.

The combined documentation would then be:

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

The code part of the PR is then three lines:

# One positional argument
range(length::Integer) = Base.OneTo(length) # Integers only!
# range(stop) = Base.OneTo(stop) # Equivalent

# range(start, stop) = range_start_stop(start, stop) # Redundant with `start:stop`
range(start, length) = range_start_length(start, length) # Julia is not Python

# Three positional argument
range(start, stop, length) = range_start_stop_length(start, stop, length)

We might almost as well just fold this into #38041 since that is also approved and the effective code is just two or three lines aliasing into non-exported functions that were created in #38041 .

@mkitti
Copy link
Contributor

mkitti commented Jan 7, 2021

I considered range( [ start, [ stop, ] ] length ). If compactness were key, it might work. Otherwise, it is challenging to read.

@mbauman
Copy link
Sponsor Member

mbauman commented Jan 7, 2021

IMO range(start, length) is a non-starter due to range(start, stop; ...). I think the "length-oriented" design is overly confusing due to that existing signature.

@mkitti
Copy link
Contributor

mkitti commented Jan 7, 2021

IMO range(start, length) is a non-starter due to range(start, stop; ...). I think the "length-oriented" design is overly confusing due to that existing signature.

I would lean towards deprecation of range(start, stop; ...) in Julia 2.0 and de-emphasizing its documentation now.
range(start, stop; length) would be covered by range(start, stop, length)
range(start, stop; step) is covered by start:step:stop
range(start, stop; step, length), with both keywords specified, causes an error

If you really want keywords galore for very specific syntax you still have
range(start; stop, step, length) for a highly backwards compatible syntax and now
range(; start, stop, step, length)

range(start, stop; ...) can still work for compatibility reasons only, but I'm failing to see why that might hinder us in the long term. It has little use after we add this and the all keyword version.

@StefanKarpinski
Copy link
Sponsor Member

I think that given that we already allow range(start, stop; length) and range(start, stop; step) we're pretty much forced to go with the "stop-oriented" design. I don't really think that either design is strictly better or worse, so I'm fairly happy to have a decision forced upon us.

@fredrikekre
Copy link
Member

For 1.0 we had a push to introduce keyword arguments in the public API (see #25188 for example) partly to avoid ambiguous cases just like this IIRC. Are people really using this function frequently enough such that having to type length= is a problem?

@mkitti
Copy link
Contributor

mkitti commented Jan 8, 2021

Note, too, that it becomes more useful if it returns a Base.OneTo — which is commonly used in high-performance axis munging.

Is it range(stop) or range(length)?

The difference is length has long been an Integer and Base.OneTo only accepts an Integer. Meanwhile, stop can be anything, or at least any Number.

Do we need to define two forms of a single argument range?

range(stop) = range_start_stop( oneunit(stop), stop ) # Equivalent to `1:stop`
range(length::Integer) = Base.OneTo( length )

Or should we restrict it to only Integer?

@mkitti
Copy link
Contributor

mkitti commented Jan 8, 2021

For three argument range(start, stop, length) can length or any of the arguments be nothing?

If length == nothing, then this just becomes equivalent to range(start, stop).
stop could be nothing if length is not nothing. This should be equivalent to range(start; length)
start could be nothing if either stop or length is defined. If only one is defined, then start defaults to 1.

@mkitti
Copy link
Contributor

mkitti commented Jan 8, 2021

I think the answers to most of my questions on nothing can be deferred to the Base._range 4-argument block in #38041

julia/base/range.jl

Lines 131 to 146 in 65898ed

_range(start::Nothing, step::Nothing, stop::Nothing, len::Nothing) = range_error(start, step, stop, len)
_range(start::Nothing, step::Nothing, stop::Nothing, len::Any ) = range_error(start, step, stop, len)
_range(start::Nothing, step::Nothing, stop::Any , len::Nothing) = range_error(start, step, stop, len)
_range(start::Nothing, step::Nothing, stop::Any , len::Any ) = range_stop_length(stop, len)
_range(start::Nothing, step::Any , stop::Nothing, len::Nothing) = range_error(start, step, stop, len)
_range(start::Nothing, step::Any , stop::Nothing, len::Any ) = range_error(start, step, stop, len)
_range(start::Nothing, step::Any , stop::Any , len::Nothing) = range_error(start, step, stop, len)
_range(start::Nothing, step::Any , stop::Any , len::Any ) = range_step_stop_length(step, stop, len)
_range(start::Any , step::Nothing, stop::Nothing, len::Nothing) = range_error(start, step, stop, len)
_range(start::Any , step::Nothing, stop::Nothing, len::Any ) = range_start_length(start, len)
_range(start::Any , step::Nothing, stop::Any , len::Nothing) = range_start_stop(start, stop)
_range(start::Any , step::Nothing, stop::Any , len::Any ) = range_start_stop_length(start, stop, len)
_range(start::Any , step::Any , stop::Nothing, len::Nothing) = range_error(start, step, stop, len)
_range(start::Any , step::Any , stop::Nothing, len::Any ) = range_start_step_length(start, step, len)
_range(start::Any , step::Any , stop::Any , len::Nothing) = range_start_step_stop(start, step, stop)
_range(start::Any , step::Any , stop::Any , len::Any ) = range_error(start, step, stop, len)

@mkitti
Copy link
Contributor

mkitti commented Feb 24, 2021

One option is the "stop-oriented" design:

  • range(stop) with start = step = 1

I'm leaving a note in this design issue that my last effort towards range(stop) in the stop-oriented design above is in #39241 which implements the keyword form range(; stop) which is a necessary precedent to range(stop).

The main motivation for range(stop) is to provide the Python user coming to Julia some familiarity as expressed in this Discourse comment.

However, range(stop), the positional form, has some outstanding questions and concerns:

  1. Can stop be any valid value for which oneunit(stop):stop works?
  2. Should stop be an Integer when given as a single argument? In Python, range(stop) only accepts integers.
  3. Can this be implemented without having strange method signatures displayed such as range(stop; stop, length, step)?
  4. Is it confusing that the single positional argument is start when given with a keyword, but stop without a keyword?
  5. Does it matter that axes and eachindex can produce an AbstractUnitRange, Base.OneTo, but range cannot create this exactly, in terms of ===?

As I do not see a path forward on range(stop) in the next few releases, I'm moving on. I'm sorry to disappoint Stefan's sympathies. Thank you for the discussion.

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