Organisation into submodules and multiple dispatch

I want to have a supermodule that contains a function, which carries out different operations depending on the input type. Depending on the type, this function is defined in various submodules. The submodules can be super complicated and standalone but here’s an MWE. Everything is in the LOAD_PATH

This is the supermodule

module SuperModule
include("SubModule1.jl")
include("SubModule2.jl")
end

These are the submodules in their .jl files in the LOAD_PATH

module SubModule1
    export foo
    foo(x::Int) = println("Hello there Int")
end

and

module SubModule2
    export foo
    foo(x::Float64) = println("Hello there Float")
end

now when I do a using SuperModule I get [ Info: Precompiling SuperModule [top-level] as expected

However, doing SuperModule.foo(1.0) or SuperModule.foo(1) gives an UndefVarError

But of course, SuperModule.SubModule1.foo(1) and SuperModule.SubModule2.foo(1.0) produce
Hello there Int and Hello there Float

I can get the desired behaviour of SuperModule.foo(x) using include, without the module and export in the submodule files. But how do I get it to work using submodules? I will not be maintaining the packages in the submodules and an include without modules will not work for me in the long run.

Thanks

1 Like

You need to add using .SubModule1, .SubModule2 somewhere in SuperModule. It is through using that exported names get imported into the current scope.

Example:

julia> module SuperModule

       module SubModule
           export foo
           foo() = println("This is SubModule.foo()")
       end

       using .SubModule

       end
Main.SuperModule

julia> SuperModule.foo()
This is SubModule.foo()
1 Like

Currently both submodules define separate functions that happen to share the name foo. Multiple dispatch isn’t working because those two foo() functions have nothing to do with one another, so there’s nothing for dispatch to do.

Fortunately, this is easy to fix. You need to define the function itself in the supermodule (you don’t need to give it any behavior, just declare that it exists), and then you need to explicitly extend that function in each submodule:

julia> module SuperModule
         # Define `foo()` but don't give it any methods yet
         function foo end
         
         # You can put this into SubModule.jl and do 
         # include("SubModule.jl") here, but I'm including 
         # it inline for this simple example.
         module SubModule1
           
          # Explicitly indicate that this is the *same* foo as in SuperModule
          import ..foo
           
          foo(x::Int) = println("hello Int")
        end
        
        module SubModule2
          import ..foo
          
          foo(x::Float64) = println("hello Float64")
        end
       end
Main.SuperModule

julia> SuperModule.foo(1)
hello Int

julia> SuperModule.foo(1.0)
hello Float64
8 Likes

This actually won’t help in the example provided above. Adding using .Submodule1, .SubModule2 will make foo unusable because both submodules export different functions with the name foo. You need the import command inside each submodule so that both submodules will extend the same function rather than defining separate ones.

Right, I thought the question was merely about exporting the names and missed the part about adding methods to a shared function.

Beautiful, that’s quite clean! Since all the submodule packages will need to provide foo, and will be independently maintained, here’s how I followed your idea one step further such that each submodule can be maintained independently of the other ones (except one module file common to all defining an abstract foo).

module SuperModule
import AbstractModule.foo # is in LOAD_PATH

# could also be through an include
module SubModule1
    import AbstractModule.foo # is in LOAD_PATH
    foo(x::Int) = println("Hello there Int")
end

# could also be through an include
module SubModule2
    import AbstractModule.foo # is in LOAD_PATH
    foo(x::Float64) = println("Hello there Float")
end

end

and AbstractModule.jl is defined as

module AbstractModule
    function foo end
end

This works nicely in the REPL

any(pwd() .== LOAD_PATH) || push!(LOAD_PATH, pwd())
using Revise, SuperModule

julia> SuperModule.foo(1)
Hello there Int

julia> SuperModule.foo(1.0)
Hello there Float

But a nagging silly question here - how did SuperModule know about foo in any of the submodules? It wasn’t exported explicitly!

Your question is more easily answered if you rewrite

import AbstractModule.foo 
foo(x::Int) = println("Hello there Int")

as

AbstractModule.foo(x::Int) = println("Hello there Int")

The second version points out more clearly that AbstractModule.foo(x::Int) = ... adds a method to the function AbstractModule.foo. That is, you do not need to export SuperModule.SubModuleX.foo() because these identifiers are simply aliases for AbstractModule.foo().

3 Likes

Ah, ok thanks. I’ve liked your response.