Julia: 1.0:5.0 should be UnitRange{Float64}

Created on 24 Aug 2020  ยท  7Comments  ยท  Source: JuliaLang/julia

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

doc

Most helpful comment

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.

All 7 comments

This is just an inconsistency in the doc. The doc for UnitRange is pretty clear that,

The syntax a:b with a and b both Integers creates a UnitRange.

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.

Was this page helpful?
0 / 5 - 0 ratings