IndirectImports.jl
IndirectImports.IndirectImports — Module.IndirectImports
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.
Example
# 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
endYou can install it with ]add IndirectImports. See more details in the documentation.
IndirectImports.@indirect — Macro.@indirect function interface_function endDeclare an 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(...) ... endDefine 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 ModuleImport 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, ..., fnImport "indirect functions" f1, f2, ..., fn. This defines constants f1, f2, ..., and fn that are extendable (see above) and callable.
@indirect function Module.interface_function(...) ... endDefine a method of an indirectly imported function. This form can be usable only in downstream modules where Module is imported via @indirect import Module.
Examples
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 @indirect:
module Upstream
using IndirectImports
@indirect function fun end
endTo define a method of an indirect function inside Upstream, wrap it in @indirect:
module Upstream
using IndirectImports
@indirect function fun end
@indirect fun() = 0 # defining a method
endStep 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 ]add Upstream⏎:
(Downstream) pkg> add UpstreamThis puts the entry Upstream in [deps] of Project.toml:
[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
Once Upstream is registered in Project.toml, you can import Upstream and define its functions, provided that they are prefixed with @indirect macro:
module Downstream
using IndirectImports
@indirect import Upstream
@indirect Upstream.fun(x) = x + 1
@indirect function Upstream.fun(x, y)
return x + y
end
endNote: It looks like defining a method works without @indirect possibly due to a "bug" in Julia [1]. 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 using https://github.com/JuliaLang/julia/issues/25744
Limitation
Function declarations can be documented as usual
"""
Docstring for `fun`.
"""
@indirect function fun endbut it does not work with the method definitions:
# Commenting out the following errors:
# """
# Docstring for `fun`.
# """
@indirect function fun()
endTo add a docstring to indirect functions in downstream packages, one workaround is to use "off-site" docstring:
@indirect function fun() ... end
"""
Docstring for `fun`.
"""
funHow 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.