I’m trying to organize a bigger project into multiple modules and I want to create function overloads specific to types found in those modules. Before all my code was in a single module and worked fine, but now my function overloads are conflicting. Why?
The problem is illustrated by the following code. It is clearly decidable which ‘f’ should be called, based on the type signature.
module A
export f
f(x::Int) = "int"
end
module B
export f
f(x::Float64) = "f64"
end
using .A, .B
f(1)
Result: WARNING: both B and A export "f"; uses of it in module Main must be qualified
Is it possible to achieve this without putting everything in a huge module, or using boilerplate?
Probably the answer is “no”, but some boilerplate can help structuring the code. One way to achieve that could be something like:
julia> module CommonFunctions
function f end
end
Main.CommonFunctions
julia> module A
import ..CommonFunctions: f
f(x::Int) = 1
end
Main.A
julia> module B
import ..CommonFunctions: f
f(x::Float64) = 1.0
end
Main.B
At least it is clear that each module extends the functionality of the shared function.
That said, in terms of code organization, I’m not sure if that helps much relative to putting the definitions in different files and include them in the main module. That is more typical as a Julia project organization.
This is actually super helpful in my case. I am trying to separate algorithms in a single package to their separate modules, but they need to meet more or less a common interface. All is a bit experimental hence I prefer the one package for now.
It is a bit weird, that with using ..CommonFunctions it does not work (there is a message that I need direct import). And sadly the importall keyword is deprecated, so I need to import manually every single function when I want to create a new module extending the interface.
With little tweaks of your solution I came up with this, at least it is free of boilerplate on the user end:
module CommonFunctions
export f
function f end
end
module A
import ..CommonFunctions: f
f(x::Int) = "int"
end
module B
import ..CommonFunctions: f
f(x::Float64) = "f64"
end
using .CommonFunctions
f(1) |> println
f(1.0) |> println
Also I didn’t know that function f end only creates the symbol, but not a callable function.
Am I correct? Julia acts like that is the case and if yes, that is quite useful for defining interfaces.
I think Julia would need some built-in non-hacky mechanism to define interfaces (e.g. without requiring complete implementation, to stick with the current ways of Julia). An interface is a great way to write self-documenting code, and the code above is actually pretty close to this.
This would be super helpful in the REPL as well when I try to figure out which functions can be used for whatever I try to do. (Yes, I know about printing signatures of a function, but sometimes that results in 30+ entries. I can’t possibly read through them and think of every type inheritance/union case as well.)
I think I found an even better way for my case, without the need for excessive boilerplate.
The Interface module has to name functions in any case (that is what an interface is for), but it has to name everything twice. Maybe some macro magic could resolve that issue. As a bonus the Interface module names all functions, so now I can look up if a function is (or what functions are) part of an interface.
This even shows explicitly when I am extending the interface functions.
The submodules can be in separate files, so code organization is not an issue.
module Interface
export f
function f end
module A
import ..Interface
Interface.f(x::Int) = "int"
end
module B
import ..Interface
Interface.f(x::Float64) = "f64"
end
end
using .Interface
f(1) |> println
f(1.0) |> println
Notice that the modules implementing the interface does not have to be inside the interface, that is just my choice for convenience when writing a package. Others can write new modules outside of Interface, but having the same access as A or B.
Inside of your top level module. Now when users “use” the module they will have access to f.
I would argue that putting implementation submodules isn’t the best approach, and putting them all next to each other in the top level module makes the most sense (as it was originally). (EDIT: It is of course viable to do this and if you prefer it, go ahead, but I think it is more common to separate it out into a flatter hierarchy.)