IndirectImports.jl lets Julia packages call and extend (a special type of) functions without importing the package defining them. This is useful for managing optional dependencies.
Compared to Requires.jl, IndirectImports.jl's approach is more static and there is no run-time
evalhence more compiler friendly. However, unlike Requires.jl, both upstream and downstream packages need to rely on IndirectImports.jl API.
Compared to "XBase.jl" approach, IndirectImports.jl is more flexible in the sense that you don't need to create an extra package and keep it in sync with the "implementation" package(s). However, unlike "XBase.jl" approach, IndirectImports.jl is usable only for functions, not for types.
# MyPlot/src/MyPlot.jl module MyPlot using IndirectImports @indirect function plot end # declare an "indirect function" @indirect function plot(x) # optional # generic implementation end end # MyDataFrames/src/MyDataFrames.jl module MyDataFrames using IndirectImports @indirect import MyPlot # this does not actually load MyPlot.jl # you can extend indirect functions @indirect function MyPlot.plot(df::MyDataFrame) # you can call indirect functions MyPlot.plot(df.columns) end end
You can install it with
]add IndirectImports. See more details in the documentation.
@indirect function interface_function end
interface_function in the upstream module (i.e., the module "owning" the function
interface_function). This function can be used and/or extended in downstream packages (via
@indirect import Module) without loading the package defining
interface_function. This from of
@indirect works only at the top-level module.
@indirect function interface_function(...) ... end
Define a method of
interface_function in the upstream module. The function
interface_function must be declared first by the above syntax.
This can also be used in downstream modules provided that
interface_function is imported by
@indirect import Module: interface_function (see below).
@indirect import Module
Import an upstream module
Module indirectly. This defines a constant named
Module which acts like the module in a limited way. Namely,
Module.f can be used to extend or call function
f, provided that
f in the actual module
Module is declared to be an "indirect function" (see above).
@indirect import Module: f1, f2, ..., fn
Import "indirect functions"
fn. This defines constants
f2, ..., and
fn that are extendable (see above) and callable.
@indirect function Module.interface_function(...) ... end
Define a method of an indirectly imported function. This form can be usable only in downstream modules where
Module is imported via
@indirect import Module.
Suppose you want extend functions in
Upstream package in
Downstream package without importing it.
Step 1: Declare indirect functions in the Upstream package
There must be a package that "declares" the ownership of an indirect function. Typically, such function is an interface extended by downstream packages.
To declare a function
fun in a package
Upstream wrap an empty definition of a function
function fun end with
module Upstream using IndirectImports @indirect function fun end end
To define a method of an indirect function inside
Upstream, wrap it in
module Upstream using IndirectImports @indirect function fun end @indirect fun() = 0 # defining a method end
Step 2: Add the upstream package in the Downstream package
Use Pkg.jl interface as usual to add
Upstream package as a dependency of the
Downstream package; i.e., type
(Downstream) pkg> add Upstream
This puts the entry
[deps] ... Upstream = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" ...
If it is not ideal to install
Upstream by default, move it to
[extras] section (you may need to create it manually):
[extras] Upstream = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
Step 3: Add method definitions in the Downstream package
Upstream is registered in
Project.toml, you can import
Upstream and define its functions, provided that they are prefixed with
module Downstream using IndirectImports @indirect import Upstream @indirect Upstream.fun(x) = x + 1 @indirect function Upstream.fun(x, y) return x + y end end
Note: It looks like defining a method works without
@indirect possibly due to a "bug" in Julia . While it is handy to define methods without
@indirect for debugging, prototyping, etc., it is a good idea to wrap the method definition in
@indirect to be forward compatible with future Julia versions.
Extending a constructor is possible with only using
Function declarations can be documented as usual
""" Docstring for `fun`. """ @indirect function fun end
but it does not work with the method definitions:
# Commenting out the following errors: # """ # Docstring for `fun`. # """ @indirect function fun() end
To add a docstring to indirect functions in downstream packages, one workaround is to use "off-site" docstring:
@indirect function fun() ... end """ Docstring for `fun`. """ fun
How it works
See https://discourse.julialang.org/t/23526/38 for a simple self-contained code to understanding the idea. Note that the actual implementation is slightly different.