Appendix A: Import syntax

You've already seen some import syntax, but now you'll see all the options available.

If you recall Chapter 1, there was this example code:

(.require
 [library
  [lux (.except)
   [program (.only program)]
   ["[0]" debug]
   [control
    ["[0]" io]]]])

Here, we're importing the library/lux module.

The (.except) option means locally import every definition exported by the library/lux module.

This allows usage of those definitions without having to give them the library/lux. prefix, or even the . shortcut prefix.

This may cause some issues if you import 2 definitions with the same name from different modules; or if you get a definition from one module, but then write your own definition with the same name in your code.

In those circumstances, the compiler will complain, saying that you can't re-define X; where X is the name of the definition.

Then, we import library/lux/program, but we only import locally the program definition. That is what the .only option allows.

There is also a `.only` option which means locally import everything **except** the specified definitions_.
You could use it like this: `[your_module (.except foo bar baz)]`

Finally, we import both the library/lux/debug and library/lux/control/io modules.

In neither case do we import any of their definitions locally.

We also give both of those modules local aliases.

That is what that "[0]" syntax does.

The .require macro recognizes that syntax for aliases and replaces the [0] with the import name directly to the right.

That means:

  • "[0]" + debug = debug
  • "[0]" + io = io

This might not seem like a big deal, but the aliasing syntax allows you to give imports arbitrary names, so long as you take into account the substitutions that may happen.

So, for example:

  • "my_[0]" + debug = my_debug
  • "[0]_[0]" + io = io_io

It is also important to note that while imports can be nested for convenience, they don't have to be.

The .require declaration could just as easily been written like this:

(.require
 [library/lux (.except)]
 [library/lux/program (.only program)]
 ["debug" library/lux/debug]
 ["io" library/lux/control/io])

You might also guess that library was not imported as a module because it was neither given an alias, not had any definitions specified as local imports.

Any module-path fragments included in the import syntax without such options will not be imported and will simply be assumed to be part of the module-paths of the sub-modules specified under them.


It is also possible to have the .require macro open interface implementations for you when importing the modules that contain them.

For example:

(.require
 [library
  [lux (.except)
   [data
    [collection
     ["[0]" list (.use "[1]::[0]" functor monoid)]]]]])

The import above would locally import:

  • list::each, from functor.
  • list::identity, from monoid.
  • list::composite, from monoid.

Here, we can also see some additional syntax for aliasing.

First of all, when opening implementations, aliasing syntax is used to determine the names of the local implementation imports.

The [0] is replaced with the name of the implementation member.

The [1] is bound to the name of the context of the import.

In this case, the implementations are coming from the library/lux/data/collection/list module, so that is the context.

And since that module has been imported with the local alias list, that is the name that replaces the [1] in the aliasing syntax for the implementation imports.

And that is how we end up with the list of names I enumerated above.

The [1] syntax for aliasing can also be used between modules, and not just when importing implementation members.

For example:

(.require
 [library
  [lux (.except)
   [data
    ["[0]" collection (.only)
     ["[1]/[0]" list (.use "[1]::[0]" functor monoid)]]]]])

Would locally import:

  • collection/list::each, from functor.
  • collection/list::identity, from monoid.
  • collection/list::composite, from monoid.

The context between module imports corresponds to the closest ancestor path which has itself been aliased.

Non-aliased paths don't count as context.

This means:

(.require
 [library
  [lux (.except)
   ["[0]" data
    [collection
     ["[1]/[0]" list (.use "[1]::[0]" functor monoid)]]]]])

Would locally import:

  • data/list::each, from functor.

  • data/list::identity, from monoid.

  • data/list::composite, from monoid.

    Also, that "_"/"ignore" syntax you may have noticed means do not import this module; just give it an alias I can refer to later as a context.

I should also note that you can both locally import definitions and open implementations as parts of the same module import.

For example:

(.require
 [library
  [lux (.except)
   [data
    [collection
     ["[0]" list (.only repeated size) (.use "[1]::[0]" monad)]]]]])

Another important feature of module imports is relative addressing, which comes in 2 flavors.

For the first one, suppose you have the following directory structure:

program
	foo
		bar
		baz
			quux
test
	foo
		bar
		baz
			quux

And you're writing code in the program/foo/baz module.

You can import other modules in the hierarchy like this:

... In program/foo/baz
(.require
 [library
  [lux (.except)]]
 ["[0]" /quux] ... program/foo/baz/quux, aliased as /quux
 ["[0]" //bar] ... program/foo/bar, aliased as //bar
 ["[0]" ///] ... program, aliased as ///
  )

A single forward slash (/) signifies "this module" in the hierarchy, so anything after the forward slash is assumed to be under this module.

Two forward slashes (//) signify "the module above", and any forward slash after that allows you to go further up the hierarchy.

In the case of program, it's enough to just specify three forward slashes (///) for the .require macro to know which module you're referring to.

You can think about it like this:

  • program/foo/baz + / + quux = program/foo/baz/quux
  • program/foo/baz + // + bar = program/foo + / + bar = program/foo/bar
  • program/foo/baz + /// = program/foo + // = program + / = program

Also, this relative path syntax can be nested, like so:

... In program/foo/baz
(.require
 [library
  [lux (.except)]]
 [/
  ["[0]" quux]] ... program/foo/baz/quux, aliased as quux
 [//
  ["[0]" bar] ... program/foo/bar, aliased as bar
  ]
 ["[0]" ///] ... program, aliased as ///
 )

Or even:

... In program/foo/baz
(.require
 [library
  [lux (.except)]]
 [/
  ["[0]" quux] ... program/foo/baz/quux, aliased as quux
  [//
   ["[0]" bar] ... program/foo/bar, aliased as bar
   ["program" //] ... program, aliased as program
   ]])

You may have noticed that when importing program, we went from /// to //.

That is because, since it's nested under another //, it's relative to program/foo instead of program/foo/baz, so only 1 step up is necessary instead of the 2 steps a /// would provide.


For the second way to do relative imports, you can see this example:

... In program/foo/baz
(.require
 [library
  [lux (.except)]]
 [\\test
  ["[0]" /] ... test/foo/baz, aliased as /
  ]
 ... Alternatively
 ["[0]" \\test] ... test/foo/baz, aliased as \\test
 ... Or
 [\\
  [\test
   ["[0]" /] ... test/foo/baz, aliased as /
   ]]
 )

The backslash (\) works in the reverse direction to the forward slash (/).

If the forward slash allows you append paths to the back, and to move up the hierarchy from the end; then the backslash allows you to append paths to the front, and the move down the hierarchy from the beginning.

Why would you want such a thing?

Because it allows you to easily establish parallel hierarchies of modules, which is a useful way to separate orthogonal aspects of your program (like the program and test hierarchies in our example).

Then, by using this relative syntax, you can refer to one hierarchy from another in an easy way.