Skip to content

Instantly share code, notes, and snippets.

@trans
Last active August 29, 2015 14:05
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save trans/39b97fcd0877f5adb050 to your computer and use it in GitHub Desktop.
Save trans/39b97fcd0877f5adb050 to your computer and use it in GitHub Desktop.
Two proposals for a better Julia Module System

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

This is my second proposal for a better module system for Julia. Reading my first proposal will probably be helpful in understanding this proposal. This proposal has the same goals as the first, but unlike the first is simpler and consequently has some limitations --but it may be that those limitation are not significant ones.

What this proposal does is equate modules and files. Thus there is no need to declare module Foo in a file. So that's convenient. But is also means that no file can have more than one module defined in it. For the sake of clarity I will call these module-files so not to confuse them with the terms of the first proposal.

Loading a module-file and using a module-file is one and the same action.

# Bar.jl
export b
function b()
  ... 
end

# Foo.jl
import Bar
function f()
   b()
 end

And just like that all of Bar's exported functions are available to Foo. We can of course load specific functions as before.

# Foo.jl
import Bar: b

To get the an "absolute" handle on a module we assign it.

# Foo.jl
Bar = import Bar: b

"Reopening" a module is a little trickier in this proposal, but it should still be possible via an absolute handle. e.g.

Bar.export("q")

Interestingly however, this could be made to effect only a local "version" of Bar vs all imports of Bar. That question is left open.

Loading files works as in the first proposal. Local files are searched first, and then packages, unless a package is clearly specified, e.g.

module Foo
  import Bar|Bar
end

The exact syntax of | not withstanding.

This proposal is clearly much simpler than the first. In fact, it is probably as simple as a module system can get, which is kind of nice. It does have some limitations, in particular that each module must be define in it own file. But that may actually be a benefit as it make it very clear where code resides.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment