I ran into a minor obstacle where I needed the UnionAll base type of a value, but I couldn’t figure out how to do it (without explicit isa checks). I.e.,
julia> p = :a => 1
:a => 1
julia> typeof(p)
Pair{Symbol, Int64}
julia> # ? something to return Pair from Pair{Symbol, Int64}?
julia> p isa Pair ? Pair : error("need to handle all used types")
Pair
julia> supertype(typeof(p)) # not good, skips the UnionAll type
Any
It’s not a big deal, but I couldn’t find a generic Core or Base call for this purpose.
Update: the accepted solution below is more like a conclusion of the discussion rather than a solution, as it relies on Julia’s internal structures instead of public APIs, which could change in future implementations.
Allow me to disagree. It could be useful in certain contexts, but I’m perfectly okay with accepting no for an answer.
That being said, it is definitely not an XY problem. I never said it was a problem in the first place, I merely called it a minor obstacle. I also explicitly stated that it can be addressed with the use of the isa operator (among others). My question was triggered by my opinion that it would be more elegant with a generic stdlib function, if such a function existed. However, there is no accounting for taste.
I could/would use it as a key in a Dict, for example, in which I group objects of a type hierarchy based on their (unparameterized) types. Like I said, this can be achieved via other means, so no hard feelings.
The crux of the issue is that “unparameterized” is arbitrary and almost meaningless. If the set of types isn’t known upfront, that is - which conflicts with your request for a generic solution. So it wouldn’t be possible to make use of such a function in generic code anyway.
It would of course be possible to implement such a function as you request, but its public interface would be ugly, and, IMO, most use of it would be misuse.
Look, I really don’t want to get into an endless theoretical debate, but I think you are making bold statements quite easily here. Why would Pair be “almost meaningless”, as you put it? Quite, the contrary, it is perfectly meaningful.
Julia implementations often use type parameters in a complex type to avoid the use of Any, thus make it concrete and be able to optimize the code that uses it. Hence its not struct Pair; a; b end, even though semantically that would work equally well. In many cases, however, the user of the type doesn’t really care what the type parameters are, as long as they work. This is recognized and appreciated by the existence of constructors without explicit type parameters, i.e., that you can write Pair(1, 2.0), and the parameters will be deduced for you. In other words, it is supported to use a UnionAll to construct a concrete object, but there is no way back from the object to the UnionAll type. One could argue that this is a reasonable decision, but it definitely results in an asymmetry, and not as obvious as you try to make it sound.
Note that for both plain and UnionAll types, TypeName(args...) does not work generically to construct a concrete object. It works for some types, but by no means is a general mechanism.
If you want to construct an instance of a type from its field values, look at ConstructionBase.jl: it’s a small package providing the constructorof(SomeType)(args...) interface.
True, it’s not a general mechanism, but it is definitely a well grounded convention. I was merely arguing that the notion of a reverse function is not that absurd.
Given the number of constructors out there which allow you to omit the type parameters, I would say there is a de facto convention. But can we at least agree that it’s a very common pattern?
The convention is to use the const name for the UnionAll to call a constructor method provided it was defined, not to compute it from a concrete parametric type first. The reason is that this only gets you the one UnionAll, what if you want Pair{Symbol} or Pair{<:Any, Int64} (perhaps with a {}-less alias)? That’s what method signatures and dispatch can do, though it’s as granular as isa checks.
That said, this is an unconventional type-processing utility, and I would prefer highlighting the very public fallback by ConstructionBase instead of the .name.wrapper approach:
@generated function constructorof(::Type{T}) where T
getfield(parentmodule(T), nameof(T))
end
This was apparently around since v0.1.0, probably before the now-preferred getglobal was available. I don’t know why it’s @generated, this pattern is for fixing a method call despite method table changes, but getfield isn’t a multimethod and neither parentmodule nor nameof are intended to be extended.
This may return (non-public) implementation details. E.g.:
module A
module B
struct Private{P} end
end
export Public
const Public = B.Private{Int}
end
In the REPL:
julia> module A
module B
struct Private{P} end
end
export Public
const Public = B.Private{Int}
end
Main.A
julia> using .A
julia> f(t::DataType) = getglobal(parentmodule(t), nameof(t))
f (generic function with 1 method)
julia> f(Public)
Main.A.B.Private