Allow syntax like Int32(1:10)?

basically,

Int32(a::UnitRange{T}) = Int32(a.start):Int32(a.stop)

It would be pretty weird if Int32(..) did not return an Int32.

8 Likes

Int32.(1:10) is the natural syntax for this sort of thing in Julia.

Unfortunately, Int32.(1:10) currently gives a dense Array object, but it seems like a straightforward optimization to update it to give a Range. For example, if we define

import Base.Broadcast: DefaultArrayStyle, broadcasted

broadcasted(::DefaultArrayStyle{1}, ::Type{T}, r::AbstractUnitRange) where {T<:Integer} = range(T(first(r)), length=T(length(r)))

then we would get:

julia> Int32.(1:10)
1:10

julia> typeof(ans)
UnitRange{Int32}

julia> BigInt.(1:10)
1:10

julia> typeof(ans)
UnitRange{BigInt}

Might make a good first PR if someone wanted to take this on.

12 Likes

ah, I think this makes sense. Would this make a breaking change though?

A range is still an AbstractArray type, and we don’t generally guarantee what type of array a broadcast will produce, so I think this would qualify as a “minor change” that could go into (e.g.) Julia 1.4. This would be discussed in a PR.

3 Likes

But people can reasonably rely on the return value of something being mutatable.

2 Likes

Just to add for completeness

Vector{Int32}(1:10)

works too

Favor convert rather than constructors,

convert(UnitRange{Int32}, 1:10)

You’re stating that like a general rule. What’s the argument? I certainly favor constructors over the very verbose constructor syntax.

Especially in cases where the point is to quickly and conveniently instantiate something at the REPL.

I’d rather Int32(1):Int32(10) in that case.

2 Likes

No, this materialize the iterator thus break the purpose.

The fundamental difference between constructors and convert is that the former makes a copy but the latter doesn’t when possible. Here it doesn’t matter since we know the returned object will be of a different type from the input, so a new object will have to be returned (and UnitRange is immutable anyway).

3 Likes

If you need mutability you should probably be safe and use

convert(Vector, Int32.(1:9))

This will work in Julia v1.0-1.4.

I know the correct way to do it if the change is made, but that doesn’t mean the change is non breaking.

1 Like

Bug fixes are also breaking changes… I think this is arguably on the level of a bug fix.

No this is definitely not a bug fix. It returns a result that makes sense and you are changing it.

2 Likes

It’s using undocumented behaviour. The fact that UInt32.(1:10) returns a Vector is simply an implementation detail, and it’s a users fault for mutating it when there is no a priori guarantee that it’s mutable.

No, that doesn’t make any sense. It’s at most missing document (and it’s not really missing either, see below). Undocumented doesn’t mean it’s implementation detail (and FWIW implementation detail could be documented as well).

It would have made sense if all other functions and syntaxes are documented to this level (what are the exact input types, what are the output types you expect, what are the output types and values under each conditions, etc).

Your argument is just that the user should never rely on anything they see, which means that they should never try type something into the REPL to see if it works since that has absolutely zero meaning. I would actually think that’s a good thing if the documentation is better (too many GC related bugs in user code from this). This would have been the case if you are talking about a specification. The documentation is not good enough to be a specification (yet) so that reasoning is wrong.

As another example, this is the documentation for cos.

Compute cosine of x , where x is in radians.

From your logic, it’s the user’s fault if he assume that cos(1)::Float64 (or even cos(1) is convertible to Float64). In fact, it’s even his fault to assume the return value is usable at all, the document doesn’t mention returning. It could compute it and return some undefined number and it’ll totally be inline with the document. It could return an RefValue or an Vector/Array containing the result just like matlab and that totally satisfies the document. These are things that must be specified in a specification but is also reasoable to left undocumented otherwise since the behavior is easy to observe and can easily be reasoned about.

And again, just to get the logic as clear as possible, I’m not saying NONE of the undocumented behaviors are implementation detail. I’m not even saying that all of the observable effects are dependable. However, it’s clearly wrong to say that undocumented implies implementation detail.

Also, in this specific case, it is understood that broadcast returns array by default. Any deviation from such a convention needs documentation.

8 Likes

For the case of a literal 1:10 (as opposed to a:b where a and b happen to be 1 and 10), one could handle this with a macro. Something like:

i32(x::Int64) = typemin(Int32) <= x <= typemax(Int32) ? Int32(x) : x
i32(x::Expr) = Expr(x.head, i32.(x.args)...)
i32(x) = x
macro i32(x)
    i32(x)
end

Now @i32 1:10 will return a UnitRange{Int32}. The macro can also be used on larger blocks of code, for example

@i32 let s = 0
    for i = 1:10
         s += i
     end
     s
end

will return an Int32

This type of macro can be useful when targeting the GPU or embedded processors where it’s otherwise easy to accidentally introduce promotion to 64-bit.

2 Likes

Wouldn’t it be better to error then for values that cannot be converted?

Also, I was under the impression that GPU interfaces sanitize their input, and then keep everything in the valid type set.