Is there a way to capture an upper bound via dispatch?

As part of the logic of writing some function, I’d like to use the upper bound of some passed-in type’s argument. Is there any way to capture this upper bound cleanly via dispatch? Basically, I’d like the following:

struct Foo{T<:Real} end

capture_upper_bound(Foo{Real})     # should return Real
capture_upper_bound(Foo{<:Real})   # should return Real
capture_upper_bound(Foo{<:Int})    # should return Int
capture_upper_bound(Foo{<:Number}) # should be a method error

The following definition of capture_upper_bound works, but the second method clearly is very hacky and not type stable and I wouldn’t really want to use it. Is there a better way to do this? Or is this one of those things that’s a lot more complex than seems at first glance and hence you can’t do with dispatch?

capture_upper_bound(::Type{F}) where {T<:Real, F<:Foo{T}} = T
capture_upper_bound(::Type{F}) where {T<:Real, F<:Foo{<:T}} = Base.unwrap_unionall(F).parameters[1].ub
1 Like

That’s about how I would’ve done it.

I’m using @pure to do constant folding on this, and that’s one of the things I want to avoid. :confused:

Where exactly are you using @pure? In my case, this doesn’t seem to make it type stable (and I think technically breaks the purity rule since it calls several generic functions):

Base.@pure capture_upper_bound(::Type{F}) where {T<:Real, F<:Foo{<:T}} = Base.unwrap_unionall(F).parameters[1].ub

Fwiw, making it @generated does make it type stable, but still curious if there’s a way to do it with dispatch.

@pure only encourages constant folding, and it does not resolve the type instability of the callee itself. Also as you said, technically it is a misuse of @pure.

I have been struggling with this issue for almost a year now and have not found a good solution.

1 Like

Maybe I’m missing something here, because this seems very straightforward:

julia> struct Foo{T<:Real} end

julia> capture_upper_bound(::Type{<:Foo{<:Real}}) = Real
capture_upper_bound (generic function with 1 method)

julia> capture_upper_bound(::Type{<:Foo{<:Int}}) = Int
capture_upper_bound (generic function with 2 methods)

julia> capture_upper_bound(Foo{Real})     # should return Real

julia> capture_upper_bound(Foo{<:Real})   # should return Real

julia> capture_upper_bound(Foo{<:Int})    # should return Int

julia> capture_upper_bound(Foo{<:Number}) # should be a method error
ERROR: MethodError: no method matching capture_upper_bound(::Type{Foo{var"#s26"} where var"#s26"<:Number})
Closest candidates are:
  capture_upper_bound(::Type{var"#s26"} where var"#s26"<:(Foo{var"#s24"} where var"#s24"<:Int64)) at REPL[3]:1
  capture_upper_bound(::Type{var"#s24"} where var"#s24"<:(Foo{var"#s26"} where var"#s26"<:Real)) at REPL[2]:1
 [1] top-level scope
   @ REPL[7]:1

Yea, I should have said more explicitly, but I’m looking for a solution that doesn’t involve hard coding any of the upper bound possibilities, so would work with all existing types (AbstractFloat, Integer, etc… in this example), as well as ones defined in the future.