# Should we disallow non-compliant \`AbstractUnitRange\`s where \`typeof(step)\` doesn't match the \`eltype\`?

**URL:** https://discourse.julialang.org/t/should-we-disallow-non-compliant-abstractunitrange-s-where-typeof-step-doesnt-match-the-eltype/101695
**Category:** Internals & Design
**Tags:** speculative, range
**Created:** [July 17, 2023, 2:58pm UTC](https://discourse.julialang.org/t/should-we-disallow-non-compliant-abstractunitrange-s-where-typeof-step-doesnt-match-the-eltype/101695 "2023-07-17T14:58:01Z")
**Posts on this page:** 8
**Page:** 1

<div class="post-metadata">

### Author: ![jishnub](https://sea2.discourse-cdn.com/julialang/user_avatar/discourse.julialang.org/jishnub/32/33620_2.png) [@jishnub](https://discourse.julialang.org/u/jishnub)
#### Post date: [July 17, 2023, 2:58pm UTC](https://discourse.julialang.org/t/should-we-disallow-non-compliant-abstractunitrange-s-where-typeof-step-doesnt-match-the-eltype/101695/1 "2023-07-17T14:58:01Z")

</div>

Currently, the docstring for `AbstractUnitRange` states:

```julia
    AbstractUnitRange{T} <: OrdinalRange{T, T}

Supertype for ranges with a step size of `oneunit(T)` with elements of type `T`.

```

This implies that the `step` must be of type `T` as well. However, this is not enforced anywhere, and as a consequence, one may create ranges for which this is not obeyed at all. Even the test suite for ranges in `Base` has some examples like this:

```julia
struct Position <: Integer
    val::Int
end
Position(x::Position) = x # to resolve ambiguity with boot.jl:770

struct Displacement <: Integer
    val::Int
end
Displacement(x::Displacement) = x # to resolve ambiguity with boot.jl:770

Base.:-(x::Displacement) = Displacement(-x.val)
Base.:-(x::Position, y::Position) = Displacement(x.val - y.val)
Base.:-(x::Position, y::Displacement) = Position(x.val - y.val)
Base.:-(x::Displacement, y::Displacement) = Displacement(x.val - y.val)
Base.:+(x::Position, y::Displacement) = Position(x.val + y.val)
Base.:+(x::Displacement, y::Displacement) = Displacement(x.val + y.val)
Base.:(<=)(x::Position, y::Position) = x.val <= y.val
Base.:(<)(x::Position, y::Position) = x.val < y.val
Base.:(<)(x::Displacement, y::Displacement) = x.val < y.val

# for StepRange computation:
Base.Unsigned(x::Displacement) = Unsigned(x.val)
Base.rem(x::Displacement, y::Displacement) = Displacement(rem(x.val, y.val))
Base.div(x::Displacement, y::Displacement) = Displacement(div(x.val, y.val))

# required for collect (summing lengths); alternatively, should length return Int by default?
Base.promote_rule(::Type{Displacement}, ::Type{Int}) = Int
Base.convert(::Type{Int}, x::Displacement) = x.val
Base.Int(x::Displacement) = x.val

```

With this definition, one may define:

```julia
julia> r = Position(2):Position(5)
Position(2):Position(5)

julia> r isa AbstractUnitRange{Position}
true

julia> step(r)
Displacement(1)

julia> step(r) |> typeof
Displacement

```

This range is, strictly speaking, a `StepRange`, with a unit step. We don’t have a `UnitStepRange` type in `Base`, which leads to the abuse of `UnitRange`s. However, disregarding type-parameters in this manner has the potential to open up unexpected bugs. Is it reasonable to disallow such non-compliant `AbstractUnitRange`s? I understand that there are concerns about the performances of `StepRange`s, but perhaps we may add a `UnitStepRange` type to get around these?

As an example, `Dates` already avoids this issue in the colon range constructor, and returns a `StepRange` instead:

```julia
julia> today():today()
Date("2023-07-17"):Day(1):Date("2023-07-17")

```

The docstring for `Colon` states that it produces

```julia
 a `UnitRange` when a and b are integers,

```

but that may be altered. IMO this should not produce a `UnitRange` if `typeof(a-b) != typeof(a)`

---

<div class="post-metadata">

### Author: ![Sukera](https://avatars.discourse-cdn.com/v4/letter/s/ce7236/32.png) [@Sukera](https://discourse.julialang.org/u/Sukera)
#### Post date: [July 17, 2023, 3:01pm UTC](https://discourse.julialang.org/t/should-we-disallow-non-compliant-abstractunitrange-s-where-typeof-step-doesnt-match-the-eltype/101695/2 "2023-07-17T15:01:58Z")

</div>

> [@jishnub](#):
>
> The docstring for `Colon` states that it produces
> 
> ```julia
> a `UnitRange` when a and b are integers,
> 
> ```
> 
> but that may be altered.

I don’t think it needs to be altered; `Day(1)` is not an integer.

> [@jishnub](#):
>
> IMO this should not produce a `UnitRange` if `typeof(a-b) != typeof(a)`

It’s fine to have `step` be different than the `eltype`; it’s only required to be of the same type as the additive element of the `eltype`. It’s similar to how you use `'a' + 1` to get `'b'` and not `'a' + Char(0x1)`.

---

<div class="post-metadata">

### Author: ![jishnub](https://sea2.discourse-cdn.com/julialang/user_avatar/discourse.julialang.org/jishnub/32/33620_2.png) [@jishnub](https://discourse.julialang.org/u/jishnub)
#### Post date: [July 17, 2023, 3:07pm UTC](https://discourse.julialang.org/t/should-we-disallow-non-compliant-abstractunitrange-s-where-typeof-step-doesnt-match-the-eltype/101695/3 "2023-07-17T15:07:48Z")

</div>

The definition `AbstractUnitRange{T} <: OrdinalRange{T, T}` requires `step` to be of the same type.

> I don’t think it needs to be altered; `Day(1)` is not an integer.

The integer example is the `Position` range from the `Base` test suite. The point actually goes the other way. It’s fine for any type to return a `StepRange` instead of a `UnitRange`, and the `Day` example was that of a type that chooses the range type correctly.

---

<div class="post-metadata">

### Author: ![Sukera](https://avatars.discourse-cdn.com/v4/letter/s/ce7236/32.png) [@Sukera](https://discourse.julialang.org/u/Sukera)
#### Post date: [July 17, 2023, 3:23pm UTC](https://discourse.julialang.org/t/should-we-disallow-non-compliant-abstractunitrange-s-where-typeof-step-doesnt-match-the-eltype/101695/4 "2023-07-17T15:23:05Z")

</div>

That’s a good point, and a bit bothersome 🤔 We don’t have a `oneadditiveunit` equivalent, unfortunately…

---

<div class="post-metadata">

### Author: ![jishnub](https://sea2.discourse-cdn.com/julialang/user_avatar/discourse.julialang.org/jishnub/32/33620_2.png) [@jishnub](https://discourse.julialang.org/u/jishnub)
#### Post date: [July 17, 2023, 4:46pm UTC](https://discourse.julialang.org/t/should-we-disallow-non-compliant-abstractunitrange-s-where-typeof-step-doesnt-match-the-eltype/101695/5 "2023-07-17T16:46:37Z")

</div>

To be more concrete about the issue in the OP: in the `Position` example, one may add a `Position` and a `Displacement` to obtain another `Position`, but there’s no notion of adding a `Position` to another `Position`, or to an `Int`. As a consequence, indexing is broken for a `UnitRange{Position}`:

```julia
julia> r = Position(2):Position(5)
Position(2):Position(5)

julia> r[2]
ERROR: promotion of types Position and Int64 failed to change any arguments

```

whereas, for a `StepRange`, if we define a few missing methods:

```julia
julia> Base.:(*)(i::Int, x::Displacement) = Displacement(x.val * i)

julia> Base.:(*)(x::Displacement, i::Int) = Displacement(x.val * i)

```

we may obtain

```julia
julia> s = Position(2):Displacement(1):Position(5)
Position(2):Displacement(1):Position(5)

julia> s[2]
Position(3)

```

where we are adding a `Displacement` to a `Position`.

---

<div class="post-metadata">

### Author: ![Sukera](https://avatars.discourse-cdn.com/v4/letter/s/ce7236/32.png) [@Sukera](https://discourse.julialang.org/u/Sukera)
#### Post date: [July 17, 2023, 5:52pm UTC](https://discourse.julialang.org/t/should-we-disallow-non-compliant-abstractunitrange-s-where-typeof-step-doesnt-match-the-eltype/101695/6 "2023-07-17T17:52:07Z")

</div>

Actually, `Position` is the odd one out here, because it subtypes `Integer`. Since `'a':'z'` is conceptually the same thing (the additive element is different from the actual type) and that already gives a `StepRange`, looking at `@edit 'a':'z'` clears things up a bit:

```julia
(:)(a::Real, b::Real) = (:)(promote(a, b)...)

(:)(start::T, stop::T) where {T<:Real} = UnitRange{T}(start, stop)

(:)(start::T, stop::T) where {T} = (:)(start, oftype(stop >= start ? stop - start : start - stop, 1), stop)

```

So `:` actually only assumes that `step` is a `T` for `Real`s, but for all others, it goes through the same `oftype` construction as `Char` goes through. `Dates` just overloads that conceptual fallback in a way that doesn’t rely on `convert(Day, ::Int)` to exist.

I guess I agree then - the docstring for `:` ought to be changed to "with a step size equal to `oftype(..., 1)` (not quite sure what to put in there).

Then again - perhaps this is just another example of underspecified requirements of functions from Base…

---

<div class="post-metadata">

### Author: ![Benny](https://avatars.discourse-cdn.com/v4/letter/b/49beb7/32.png) [@Benny](https://discourse.julialang.org/u/Benny)
#### Post date: [July 17, 2023, 6:14pm UTC](https://discourse.julialang.org/t/should-we-disallow-non-compliant-abstractunitrange-s-where-typeof-step-doesnt-match-the-eltype/101695/7 "2023-07-17T18:14:24Z")

</div>

I’d say it _should_ be allowed because differences of quantities like dates can’t be treated the same as the absolute values, I’m sure there’s a formal math term for numbers like that. Problem is I’m not sure how to patch this without changing the type parameters to `AbstractUnitRange{T, S}`, which seems breaking. It’d be nice to have `AbstractUnitRange{T} <: OrdinalRange{T, S}` where `S` is promised to have a 1-to-1 correspondence with `T` in `AbstractUnitRange`s.

---

<div class="post-metadata">

### Author: ![jar1](https://avatars.discourse-cdn.com/v4/letter/j/c0e974/32.png) [@jar1](https://discourse.julialang.org/u/jar1)
#### Post date: [July 17, 2023, 8:13pm UTC](https://discourse.julialang.org/t/should-we-disallow-non-compliant-abstractunitrange-s-where-typeof-step-doesnt-match-the-eltype/101695/8 "2023-07-17T20:13:33Z")

</div>

> [@1 + 'a' = 'b'](https://discourse.julialang.org/t/1-a-b/77947/35):
>
> For the mathematically inclined, +(::Char,::Int) forms a [group action](https://www.wikiwand.com/en/Group_action) of the (additive) group of integers on the set of characters. Similarly +(::Ptr{T},::Int) is a group action of integers on pointers and +(::Int,::Int) is the usual group action of integers on themselves, namely the (additive) group operation on integers. So this is all consistent and legit from a mathematical point of view.

But probably should be

> `'a' + NextCharacterInTermsOfUnicodeCodepoints(1)`

> [@1 + 'a' = 'b'](https://discourse.julialang.org/t/1-a-b/77947/82):
>
> This is a very interesting discussion. I especially like the mathematical explanation by @cjdoris considering it as group action. For me the main concern seems to be that the group action is just denoted by an integer. For instance, in the example Date(2022, 3, 16) + Day(1) it is clear that Day(1) denotes a time interval/shift. In this interpretation 'a' + 1 should be understood and maybe explicitly written as 'a' + NextCharacterInTermsOfUnicodeCodepoints(1) being open to suggestions for a…
