Is there a way to get a UnionAll base type from a concrete type?

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.

Out of curiosity, what do you need that type for? There’s generally not much of anything meaningful you can actually do with that.

1 Like

Seems like an XY problem:

https://xyproblem.info

Try taking a step back and describe your actual goals.

Doubtful.

And that’s a good thing, such functionality wouldn’t be useful. It wouldn’t even be meaningful if the types aren’t hardcoded.

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.

You can use

julia> typeof(Pair(1,3)).name.wrapper
Pair

That being said, this is a very niche requirement and somewhat of a light smell that you’re about to do something ill-considered.

3 Likes

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.

Yeah, no, that would be ugly, and would break easily.

1 Like

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.

SparseDiffTools.jl (part of the SciML ecosystem) uses this construction, but it’s also based on .wrapper.

4 Likes

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.

3 Likes

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.

This shouldn’t be marked as the solution, as that would mislead others into thinking the implementation is acceptable for general package code.

Could you at least edit in a warning about this being unsupported?

There’s no such convention.

Done.

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?

1 Like

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.

1 Like

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

And it’ll throw if one, .e.g., passes it an Union. EDIT: a PR: better error message when the `constructorof` fallback is passed a `Union` by nsajko · Pull Request #97 · JuliaObjects/ConstructionBase.jl · GitHub

Submitted a PR: make the `constructorof` fallback method non-`@generated` by nsajko · Pull Request #98 · JuliaObjects/ConstructionBase.jl · GitHub