Dispatch.jl - A Julia package that makes function dispatching across modules simple, flexible, and efficient

dispatch-jl

Dispatch.jl is a Julia package that provides macros for dynamic and static dispatch of function calls based on the types of the input arguments. This can be useful in scenarios where multiple modules define functions with the same names, and the appropriate function needs to be called based on the types of input arguments.

Example

Tests

Imagine you have Api module and two implementation modules: ModuleA and ModuleB

Api:

module Api
export Interface, foo

abstract type Interface end

function foo(i::Interface, s::String, n::Int)
    error("not implemented")
end

end

ModuleA:

module ModuleA

include("Api.jl")

using .Api

export Interface
export A, foo

struct A <: Interface end

function foo(i::A, s::String, n::Int)
    println("A::echo(s=$s, n=$n)")
end

end

ModuleB:

module ModuleB

include("Api.jl")

using .Api

export Interface
export B, foo

struct B <: Interface end

function foo(i::B, s::String, n::Int)
    println("B::echo(s=$s, n=$n)")
end

end

Dispatch.jl provides two macros:

@dynamic_dispatch

The @dynamic_dispatch macro dynamically dispatches function calls based on the type of the first argument. Here’s how to use it:

using Dispatch
include("Api.jl")
include("A.jl")
include("B.jl")

using .Api
using .ModuleA
using .ModuleB
import .Api: foo

@dynamic_dispatch(Main.Api.foo)

foo(A(), "a", 1)
foo(B(), "b", 2)    

the macro will generate a code like the below:

begin
    function foo(i, s, n)
        var"#6#obj" = i
        var"#7#m" = Dispatch.parentmodule(Dispatch.typeof(var"#6#obj"))   
        return (var"#7#m").foo(i, s, n)
    end
end

@static_dispatch

The @static_dispatch macro generates specialized methods for a given function and a list of argument types. Here’s how to use it:

using Dispatch
include("Api.jl")
include("A.jl")
include("B.jl")

using .Api
using .ModuleA
using .ModuleB
import .Api: foo

@static_dispatch(Main.Api.foo, [
    Main.ModuleA.A,
    Main.ModuleB.B,
])

# or relative path
# @static_dispatch(Api.foo, [
#     ModuleA.A,
#     ModuleB.B,
# ])

foo(A(), "a", 1)
foo(B(), "b", 2)   
    

the macro will generate methods for A and B, i.e.:

function foo(i::A, s::String, n::Int)
    ModuleA.foo(i, s, n)
end

function foo(i::B, s::String, n::Int)
    ModuleB.foo(i, s, n)
end

Choosing Between @dynamic_dispatch and @static_dispatch

Use @dynamic_dispatch during development: When exploring different implementations or when the set of argument types may change during development, @dynamic_dispatch provides flexibility and ease of use.
Consider switching to @static_dispatch for production: Once the set of argument types is stable and performance becomes a concern, consider switching to @static_dispatch to minimize dispatch overhead and improve performance.

I am bit confused by your package: Can’t you do the exact same thing without macros and just a slight adjustment of the project’s structure?

Usually how packages are structured is like this:
api.jl (basically identical to yours)

module Api
export Interface, foo

abstract type Interface end

# slightly more idiomatic: just declare the function and 
# let Julia raise a MethodError if an implementation is missing
# Sidenote: Good style is to put your docstring here
"""
   foo(interface, s, n)

Foo your interface given a String s and an Int n.
"""
function foo end 
end

moduleA.jl

module ModuleA

using ..Api # note the additional .

export A # don't reexport foo or Interface

struct A <: Interface end

# attach method to Api's foo function
# note we have to qualify the function's name with the module
# because we used `using .Api`
function Api.foo(i::A, s::String, n::Int)
    println("A::echo(s=$s, n=$n)")
end

end

moduleB.jl (uses import and thus can leave names unqualified)

module ModuleB

import ..Api: Interface, foo

export B # don't reexport foo or Interface

struct B <: Interface end

# attach method to Api's foo function
function foo(i::B, s::String, n::Int)
    println("B::echo(s=$s, n=$n)")
end

end

new main file of the package MyPackage.jl

module MyPackage

export foo, A, B

include("api.jl")
include("moduleA.jl")
include("moduleB.jl")

using .Api
using .moduleA
using .moduleB

end

Then we can do:

julia> include("MyPackage.jl")
Main.MyPackage

julia> b = MyPackage.B()
Main.MyPackage.moduleB.B()

julia> MyPackage.foo(b, "asdf", 3)
B::echo(s=asdf, n=3)

julia> a = MyPackage.A()
Main.MyPackage.moduleA.A()

julia> MyPackage.foo(a, "asdf", 3)
A::echo(s=asdf, n=3)

Maybe I missed your point though. Can you elaborate?

1 Like

The previous comment’s refactoring implies it, but your example has 2 modules with 2 separate functions of the same name, similar to how 2 classes in a OOP language may encapsulate 2 separate methods of the same name. Julia’s multiple dispatch uses the possibly multiple argument types to select one of the possibly multiple methods of one function, not several functions. The package makes a dispatch system closer to OOP languages, but it’s not idiomatic in Julia.

2 Likes