I definitely agree that having the module own the type is necessary. In most situations I think that additionally subtyping an abstract type would really be best practice.
That is, like the following example.
(Using FromFile rather than modules since that’s my preference, but mentally substitute the usual include/import boilerplate if you prefer.)
# foo.jl
abstract type AbstractFoo end
function accepts_foo(x::AbstractFoo)
error("Must define `accepts_foo` for type `$(typeof(x)) <: AbstractFoo`")
end
# foobar.jl
using FromFile: @from
@from "foo.jl" import AbstractFoo, accepts_foo
struct Foobar <: AbstractFoo
...
end
function accepts_foo(::Foobar)
...
end
This gives at least some discoverability to track what’s going on.
This analogous to overriding methods of a class in typical single-dispatch OO; just extending it out to multiple dispatch in the obvious way.
Don’t forget that proper importing of symbols helps with a lot more than just this. Even if you don’t know the method you at least know which function is being called. Code exists in smaller namespaces so it’s harder to introduce various classes of bugs. Topologically sorting dependencies is no longer a burden placed upon the developer. And FromFile, at least, additionally handles deduplication (no include-ing a module twice). Etc. etc. you get the idea.