In Julia 1.5.0, the help text of : promises
help?> :
(:)(start, [step], stop)
Range operator. a:b constructs a range from a to b with a step size of 1 (a
UnitRange) , and a:s:b is similar but uses a step size of s (a StepRange).
However, at present a:b is not acually a UnitRange if a and b are floats:
julia> isa(1.0:5.0,UnitRange)
false
julia> typeof(1.0:5.0)
StepRangeLen{Float64,Base.TwicePrecision{Float64},Base.TwicePrecision{Float64}}
Expected behaviour:
julia> 1.0:5.0 === UnitRange{Float64}(1.0,5.0)
true
The culprit appears to be the line
# AbstractFloat specializations
(:)(a::T, b::T) where {T<:AbstractFloat} = (:)(a, T(1), b)
in base/range.jl, the purpose of which I don't understand. It appears to override the line
(:)(start::T, stop::T) where {T<:Real} = UnitRange{T}(start, stop)
The reason this is a concern:
julia> foo(subrange::UnitRange{<:Real}) = last(subrange)
foo (generic function with 1 method)
julia> foo(1.0:5.0)
ERROR: MethodError: no method matching foo(::StepRangeLen{Float64,Base.TwicePrecision{Float64},Base.TwicePrecision{Float64}})
This is just an inconsistency in the doc. The doc for UnitRange is pretty clear that,
The syntax
a:bwithaandbboth Integers creates aUnitRange.
But as long as UnitRange{Float64} can be created by calling the constructor, does it not make more sense for 1.0:5.0 to mean the same as UnitRange(1.0, 5.0)?
What is the reason for discouraging the use of UnitRange{Float64}? After all, the number 1 has an exact representation in all commonly used floating-point representations, so there are no worries about precision. The float 1.0 is just as exact a representation of 1 as the integer 1 and Float64 can exactly represent all integers from -2^53 to 2^53. In other words, IEEE 64-bit floating-point numbers are at least as accurate as 53-bit integers, which in turn are more than good enough for many applications (e.g. any array indexing on CPUs with 48-bit virtual memory address space, such as x86-64 with 4-level paging).
This issue emerged in the context of the WAV.jl package, which implements audio-file methods compatible with the old MATLAB wavread() functions. Coming from MATLAB where all numeric literals are floats by default, there was a desire to accept a UnitRange{Float64} also in the Julia implementation, because wavread("filename.wav", fs; subrange=start_time*fs:end_time*fs) is a common use case, where start_time::Float64 and end_time::Float64. I suggested to constrain subrange to (a union type including) ::UnitRange{<:Real}, because the implementation doesn't support non-unit ranges, and the lack of such an assertion had caused some user confusion (dancasimiro/WAV.jl#78). But I was surprised that : cannot actually create a UnitRange{Float64}, contrary to its documentation, hence adding the UnitRange{<:Real} type assertion I had suggested became infeasible.
(There may well sometimes be performance advantages of Int64 over Float64 on CPUs that have more ALUs for integers than floats in their pipelines, but these don't matter for this example application, which only reads first(subrange) and last(subrange) rather than iterating over subrange.)
You can't increment all floats by 1, so the notion of a Float-based UnitRange is a little shaky. Consider:
julia> x = UnitRange{Float64}(2^53, 2^54)
9.007199254740992e15:1.8014398509481984e16
julia> x[1] === x[2]
true
julia> allunique(x) # wrong!
true
That may be fine enough for some use cases that only goes up to 2^52, but in the face of that inconsistency, it's probably fine that the user needs to be explicit about wanting a float-based UnitRange.
Wouldn't a simple safety-check along the lines of
function UnitRange(a::T, b::T) where {T<:AbstractFloat}
max = T(2)^precision(T)
abs(a) <= max || throw(InexactError(:UnitRange, UnitRange{T}, a))
abs(b) <= max || throw(InexactError(:UnitRange, UnitRange{T}, b))
...
make โthe notion of a Float-based UnitRangeโ pretty solid?
(Edit: actually that check only works well if isinteger(a))
there was a desire to accept a UnitRange{Float64} also in the Julia implementation,
No don't do that. We have proper integer support and you should not disseminate that for the same reason we do not allow indexing using floating point numbers.
OK. I'll leave the issue open then until the related documentation inconsistency for : is fixed.
Most helpful comment
You can't increment all floats by 1, so the notion of a Float-based
UnitRangeis a little shaky. Consider:That may be fine enough for some use cases that only goes up to 2^52, but in the face of that inconsistency, it's probably fine that the user needs to be explicit about wanting a float-based
UnitRange.