This is one of two proposals for a better module system for Julia. The goal is to promote good modular design. This proposal, as opposed to my other proposal, supports multiple modules per file.
The main concept is that files are loaded into a program always at the toplevel. Once loaded any module within those files are available for use, either through their absolute paths or by way of importing.
I will use the function load
in the examples, but obviously another term can be used. When a file is loaded, it's modules are made available as are it's exported functions via absolute paths. e.g.
# Bar.jl
module Bar
function f()
...
end
end
# Foo.jl
load Bar
module Foo
function g()
Bar.f()
end
end
Any toplevel code in a file, i.e. outside the scope of a module or function, is processed only once on first load at compile time. This can be used to create computed resolutions, such as dynamic loads. e.g.
if something
load(something)
end
With parentheses load
works like a function. So in the above, something
is a variable. Without parentheses load
is more like keyword and treats the following text as if it were a function argument in quotes, i.e. load foo
-> load("foo")
. It also assumes an extension of .jl
. Multiple terms separated by .
are converted to /
, hence directory separations. So load Foo.Bar
looks for Foo/Bar.jl
. (Whether letter case matters can be decided latter. It makes no difference to this proposal.)
All loads first look to the local directory of the loading file for a match. If not found it then looks to installed packages. Name clashes between local files and packages are generally easy enough to avoid by choosing non conflicting names. It a name conflict is unavoidable a "package marker" can be use to distinguish the package from the file. e.g. a |
between the package name and file name (the exact marker to use is an open question).
load Foo|Bar
Of course in most cases that will be load Foo|Foo
which sucks for redundancy, but maybe someone else can think of a good way to denote that without the duplication. Also, local relative paths can be specified by prefixing ./
and ../
if necessary, but generally these should not be needed.
As an alternative to the use of the |
it could require a "from" term, e.g.
load Bar from Foo
That reads a little better, albeit it not as concise.
I will use the term import
to serve as the name of the function that brings module functions into the scope of other modules. I think the is a good term b/c it contrasts well with export
.
# Bar.jl
module BarA
export ga
function ga() ...
end
module BarB
export gb
function gb() ...
end
# Baz.jl
module Baz
export h
function h() ...
end
# Foo.jl
load Bar
load Baz
module Foo
import BarA
import BarB
function f()
ga()
gb()
end
end
By importing BarA
and BarB
, their exported functions are made visible within Foo without absolute paths. There doesn't need to be a separate using
, as import
allows the methods to both be found and to be extended. If there is a name clash between imported modules the later wins out. Specific methods can be imported by using the colon notation.
module Foo
import BarA: ga
end
This would import only ga
and no other functions. Functions must be exported to be accessed. The only way to access none exported function would be to force them, by reopening the module and modifying it. Which beings me to last part of this proposal.
If a module is "reopened" then it can be modified. This happens at compile time, so it is safe.
# Bar.jl
module Bar
function q() ...
end
# Foo.jl
load Bar
module Foo
function f()
Bar.q()
end
end
module Bar
export q
end
So Foo.f()
can work b/c we modified Bar to allow it. This of course should be done with clear understanding of what one is doing. If it is being done to a module from another package b/c the author probably didn't export the function for a reason. But it is important to be able to have this option b/c it makes it possible to fix overlooked limitations and bugs, and improves potential code reuse.
The advantages of this design are essentially all the advantages of modular programming since that is what it is designed to provide. One nice thing that stands out is that all loads can go at the top of a file, as order of loads is not significant. That makes it much easier to see what a file requires. It also means the each file can have it's own loads even if they are the same as another files that loads it. They are only ever loaded once.
# Foo.jl
load Bar.jl
load Baz.jl
# Bar.jl
load Quaz.jl
# Baz.jl
load Quaz.jl