Regarding your @inline
workaround. This will work sometimes, because inlining makes constant propagation more potent. But it’s only a hint and may not work in all cases. If the module is not a known constant at the call site, you can still end up in a similar situation to the original (except with the dynamic dispatch happening within the ::Module
method rather than the ::Type{Mod}
method). The function barrier this introduces may result in different performance characteristics (often an improvement).
A more idiomatic pattern similar to what you’re doing might be this (I use slightly different patterns for A
and B
, you can choose which you prefer):
module ABCore
export foo
function foo end # function stub we'll add to later
end
module A
using ..ABCore # may need to fiddle with the number of `.` depending on your context
export AA
struct AAType end # singleton type
AA = AAType() # construct an instance
ABCore.foo(::AAType, bar) = bar + bar # add a method
end
module B
using ..ABCore # may need to fiddle with the number of `.` depending on your context
export BB
struct BB end # singleton type
# here I use a slightly different pattern where BB is a type, which will change how this is called
ABCore.foo(::BB, bar) = bar * bar # add a method
end
julia> using .ABCore, .A, .B
julia> foo(AA, 3) # AA is an instance, so call like this
6
julia> foo(BB(), 3) # BB is a type, so use () to construct an instance
9
julia> methods(foo) # all methods of the same function now
# 2 methods for generic function "foo" from Main.ABCore:
[1] foo(::BB, bar)
@ Main.B REPL[3]:9
[2] foo(::Main.A.AAType, bar)
@ Main.A REPL[2]:10
In this pattern, rather than passing around Module
s, you use the singleton type provided by each module to control which method of foo
is called. There is only a single foo
function, but different modules create different methods to implement it.