Limit subtype to immediate parent in method signature?

In UnitTypes I need to restrict a method’s arguments to types that all subtype the same parent, though there are multiple families of parents and child types. Here’s an example:

abstract type AbstractMeasure end

abstract type AbstractLength <: AbstractMeasure end
struct MilliMeter <: AbstractLength
  value::Number
end
finb = 1/1000
struct Inch <: AbstractLength
  value::Number
end
finc = 0.0254

abstract type AbstractSound <: AbstractMeasure end
struct Growl <: AbstractSound
  value::Number
end
struct Roar <: AbstractSound
  value::Number
end

This yields the type tree:

# AbstractMeasure
#   AbstractLength
#     MilliMeter
#     Inch
#   AbstractSound
#     Growl
#     Roar

It makes sense to convert children of AbstractLength or AbstractSound, but converting a length into a sound should error. Since users are free to add additional measures under AbstractMeasure, I want to write the convert() once and have it be correctly restricted to measures of Length or Sound or etc while disallowing cross-conversion.

This would be nice but it fails:

function connie(::Type{T}, y::U) where {T<:AbstractMeasure, U<:supertype(T)}
  return T(y.value * finc/finb)
end
@show connie(Growl, Inch(3.4))

Layering wheres should work,

function connie(::Type{T}, y::U) where {T<:N, U<:N} where N
  return T(y.value * finc/finb)
end
@show connie(Growl, Inch(3.4))

but <: just reaches up to AbstractMeasure or Any instead of stopping at the immediate parent, AbstractLength.

code_warntype(connie)
# MethodInstance for connie(::Type{T}, ::U) where {N, T<:N, U<:N}
#   from connie(::Type{T}, y::U) where {N, T<:N, U<:N} @ Main ..\typesDev.jl:29
# Static Parameters
#   N <: Any
#   T <: N
#   U <: N 
# Arguments
#   #self#::Core.Const(Main.connie)
#   _::Type{T} where {N, T<:N}
#   y::Any
# Body::Any
# 1 ─ %1 = $(Expr(:static_parameter, 2))::Type{T} where {N, T<:N}
# │   %2 = Main.:/::Core.Const(/)
# │   %3 = Main.:*::Core.Const(*)
# │   %4 = Base.getproperty(y, :value)::Any
# │   %5 = Main.finc::Any
# │   %6 = (%3)(%4, %5)::Any
# │   %7 = Main.finb::Any   
# │   %8 = (%2)(%6, %7)::Any
# │   %9 = (%1)(%8)::Any    
# └──      return %9

Similarly introducing another layer

function connie(::Type{T}, y::U) where {T<:N, U<:N} where N<:O where O<:AbstractMeasure
  return T(y.value * finc/finb)
end
# MethodInstance for connie(::Type{T}, ::U) where {O<:AbstractMeasure, N<:O, T<:N, U<:N}
#   from connie(::Type{T}, y::U) where {O<:AbstractMeasure, N<:O, T<:N, U<:N} @ Main w:\mechgits\dev\251013_unitTypesFaster\typesDev.jl:31
# Static Parameters
#   O <: AbstractMeasure
#   N <: O<:AbstractMeasure
#   T <: N<:O<:AbstractMeasure
#   U <: N<:O<:AbstractMeasure
# Arguments
#   #self#::Core.Const(Main.connie)
#   _::Type{T} where {O<:AbstractMeasure, N<:O, T<:N}
#   y::AbstractMeasure
# Body::Any
# 1 ─ %1 = $(Expr(:static_parameter, 3))::Type{T} where {O<:AbstractMeasure, N<:O, T<:N}
# │   %2 = Main.:/::Core.Const(/)
# │   %3 = Main.:*::Core.Const(*)
# │   %4 = Base.getproperty(y, :value)::Any
# │   %5 = Main.finc::Any
# │   %6 = (%3)(%4, %5)::Any
# │   %7 = Main.finb::Any
# │   %8 = (%2)(%6, %7)::Any
# │   %9 = (%1)(%8)::Any
# └──      return %9

makes N and O both AbstractMeasure while AbstractLength is again skipped over.

Is there any way to limit how far subtype reaches?

Thanks

If the idea is to just throw an error with cross-conversions, you could perhaps do somthing like this? Though, I’m not sure what’s going to happen in more complicated cases.

function connie(::Type{T}, y::U) where {T<:AbstractMeasure, U<:AbstractMeasure}
  @assert typejoin(T,U) <: supertype(T) "Can't convert $U to $T"
  return T(y.value * finc/finb)
end
@show connie(Growl, Inch(3.4))

Thanks @sgaure I think I’ll have to do that.

Summarizing my attempts,

  • macros fail because they evaluate at parsing, so they don’t have any greater knowledge of the function types than the author. This can be seen in
  macro emitFunctions(expr)
    display(dump(expr))
  end
  @emitFunctions connie(::Type{T}, y::U) where {T<:AbstractMeasure, U<:supertype(T) }
  connie(MilliMeter, Inch(3.4)) # == 3.4mB

where the expr tree is only made up of T and U, not the types as later invoked.

  • @generated fails because it can only affect the method’s body, not its signature
  @generated function connie(::Type{T}, y::U) where {T<:AbstractLength, U<:AbstractLength}
    # T := DataType
    # y := Inch
    # U := DataType
    return :(T(y.value * finc/finb) )
  end
  connie(MilliMeter, Inch(3.4)) # == 3.4mB

that is nothing I do in the function body to build the quoted return will change the signature, and that’s the part that needs to be changed to correctly dispatch.