Automatically generate default instances for all concrete subtypes of a type

TL; DR: for a given abstract type, generate a vector/tuple/whatever containing a default instance for each of its concrete subtypes.

un-XY: I have several concrete types that subtype an abstract supertype (possibly indirectly), and each of these concrete types defines two methods, for which I would like to check that they agree (let’s say in the “default” case). As such, I would like to

  • iterate over all concrete types that subtype my abstract type
  • register a get_default() function for each of these concrete types that gives me a default instance, def, for the given concrete type
  • for def, run some routine to verify that the methods are consistent
  • as soon as an inconsistency is detected, error

To illustrate in a MWE (that won’t run because I don’t know how to define a few components…)

using Random
Random.seed!(42)
abstract type Abs_type end
abstract type Abs_sub1 <: Abs_type end
struct A <: Abs_type end
struct B <: Abs_sub_1 end
struct C <: Abs_sub_1 end

# Method 1 and method 2 should be such that method1(x, ::Abs_type) + method2(x, ::Abs_type) == 0
## Consistent definition for A
method1(x, ::A) = x
method2(x, ::A) = -x
## Consistent definition for B
method1(x, ::B) = -x
method2(x, ::B) = x
## Inconsistent definition for C
method1(x, ::C) = x
method2(x, ::C) = 0 * x

function test(N)
    concrete_subtypes = get_concrete_subtypes(Abs_type) # How to get these? 
    x = rand(N)
    for s in concrete_subtypes
        s_def = get_default(s) # How to get these? I know I'll have to define get_default for each concrete subtype, but how do I dispatch on these?
        @assert all(method1(xi, s) + method2(xi, s) ≈ 0 for xi in x) 
    end
    return nothing
end

(I won’t be using this in any performant calculation, it will instead serve as automated checks that all methods remain consistent if I’ve been tinkering around with some of them)

1 Like
julia> using AbstractTrees
 
julia> struct TypeTree
           type::Type
       end
 
julia> function AbstractTrees.children(tree::TypeTree)
           Iterators.map(TypeTree, subtypes(tree.type))
       end
 
julia> abstract type A end
 
julia> struct P <: A end  # finitely-many subtypes
 
julia> struct Q <: A end  # finitely-many subtypes
 
julia> struct R{X} <: A end  # infinitely-many subtypes!
 
julia> leaf_subtypes = Iterators.map((x -> x.type), Leaves(TypeTree(A)));
 
julia> concrete_leaves = Iterators.filter(isconcretetype, leaf_subtypes);
 
julia> collect(Type, leaf_subtypes)
3-element Vector{Type}:
 P
 Q
 R
 
julia> collect(Type, concrete_leaves)
2-element Vector{Type}:
 P
 Q

Not sure what you mean.

1 Like

Thank you, that solves the first part!

For s_def = get_default(s), what I mean is that I could define:

get_default(::A) = A()
get_default(::B) = B()
etc.

(which is a bit silly as my example didn’t include fields). However, the values in your leaf_subtypes are of type Type, so I can’t dispatch on them, right?

1 Like
get_default(::Type{A}) = A()
get_default(::Type{B}) = B()
1 Like

Thanks, that all works like a charm!

1 Like

One thing to keep in mind is:

julia> @which subtypes
InteractiveUtils

So, if you’re doing this in a non-interactive context, like a script or a test suite, you’ll need to import InteractiveUtils, for example like using InteractiveUtils: subtypes.

1 Like