I like the Ref version. It feels both shorter and more natural. I think every constructor that follows Type(x::X) where X = new{X}(x) is currently broken when called via Type(typeof(x)).
The erratic error convert message I described can for instance be observed via:
function foo()
rx = Ref(typeof(1))
y = Number[1][1]
ry = Ref(typeof(y))
[rx][1] = ry
# ERROR: MethodError: Cannot `convert` an object of type
# Base.RefValue{DataType} to an object of type
# Base.RefValue{DataType}
# (followed by more details...)
end
foo()
So I guess it is indeed generally best to stay away from this. This and the prior example basically boils down to
typeintersect(Base.RefValue{Type{Int}}, Base.RefValue{DataType}) being empty as opposed to typeintersect(Base.RefValue{Type{Int}}, Base.RefValue{<:DataType}), and the compiler forgetting that indeed DataType can have nontrivial subtypes.
While there are other cases (say trying to manually check the types within a union vector) that do not break anything, I wonder if the performance is tied to aboves actual bug. See for instance:
function test()
n = 1000
f = x -> map(typeof, x)
v = fill(Union{Float64, Int}[1, 1], n)
@time map(f, v) # 0.002802 seconds (13.01 k allocations: 461.383 KiB)
println(typeof(map(f, v))) # Vector{Vector{DataType}}
v = fill([1, 1], n)
@time map(f, v) # 0.000013 seconds (2.00 k allocations: 86.008 KiB)
v = fill(Any[1, 1], n)
@time map(f, v) # 0.000013 seconds (2.00 k allocations: 86.008 KiB)
return nothing
end
test()
I understand that in many cases, typeof particular behaviour throughout compilation is important. Yet while the output of map is just the same in all cases, it is ironically 100 times slower for the Union than the Any case, since at some point, the compiler tries to infer the possible return types of typeof more specifically than just DataType, even though the true return type of the map call Vector{DataType} is itself type-stable.
Unfortunately, after map, this information is not even available to the compiler, but the compiler can not predict that all subfunctions in map are just called for that very result:
function _test()
n = 1000
f = x -> map(typeof, x)
v = fill(Union{Float64, Int}[1, 1], n)
m1 = map(f, v)
v = fill([1, 1], n)
m2 = map(f, v)
v = fill(Any[1, 1], n)
m3 = map(f, v)
return nothing
end
@code_warntype _test()
# (...)
# m3::Vector{Vector{DataType}}
# m2::Vector{Vector{DataType}}
# m1::Vector
# (...)
One can quickly fix this by first initiating Vector{DataType}(undef, n). Funny enough, one can also wrap the type into a tuple via f = x -> map(e->tuple(typeof(e)),x), which lets all versions run equally fast as well.
So again, this particular case has an easy fix, but it kind of wants me to see a very bright warning in the documentation of typeof or within the performance tips section. The biggest problem I see at the moment is the difficulty to predict the performance of typeof in different situation, in particular in combination with tuples:
function test()
v = Union{Float64, Int}[1, 1]
t = (typeof(v[1]),)
val1 = Val{t[1] <: Number}()
val2 = Val{typeof(v[1]) <: Number}()
end
@code_warntype test()
# val2::Val{true}
# val1::Val
# t::Tuple{Type}
So julia can not manage to carry over the narrowed types through the Tuple, which leads right into the much discussed rabbit hole that Tuple{Int} <: (Tuple{Type{T}} where T <: Union{Int,Float64}) curiously is false (which it really should not be, but I think this is for some very deep implementation reasons…?).
So there is currently no chance for julia to narrow down the type of t using some Type{...}, but I do not know why julia should currently fail to infer that at least t::Tuple{DataType}.
Unfortunately, some behaviour and documentation within julia can lead to some wrong assumptions on the behaviour of DataType as well:
isconcretetype(DataType) # true
subtypes(DataType) # Type[] (although what follows is...)
Type{Int} <: DataType # true
(Type{T} where T) <: DataType # false (yet Type{T} <: DataType for every T)
subtype(Type{Int}) # 1-element Vector{Any}: Type{Int} (in turn returns the type itself)
subtype(Int) # Type[] (in turn does not return Int)
Base.return_types(typeof) # 1-element Vector{Any}: DataType
<:(T1, T2)::Bool
Subtyping relation, defined between two types. In Julia, a type S is said to be a subtype of a type T if and only if we have S <: T.
subtypes(T::DataType)
Return a list of immediate subtypes of DataType T. Note that all currently loaded subtypes are included, including those not visible in the current module.