To create a module, use the module special form. All modules must start with a module declaration which provides the module with a name, documentation string and an exports vector. Module declarations are evaluated, so you must quote any exports.

Here is a simple example of a module:

  {:module  'my-module
   :doc     "Just for showing off modules."
   :exports '[foo bar]}

  (def foo-helper
    (fn [x] (+ x 1)))

  (def foo
    (fn [x] (foo-helper (foo-helper x))))

  (def bar
    (fn [x] (foo (+ x 2)))))

To import a module, use import:

(import my-module)

The content of a module is evaluated in a new scope that is identical to the scope of the module definition site. This means you you can use whatever has already been defined before the module was, and that definitions in the module don’t leak outside the module. The form returns the module value, which can be passed around just like any other value. This means that we can write functions which create modules:

(def make-taxes
  (fn [VAT]
     {:module 'taxes
      :doc    "Calculates taxes."
      :exports ['add-VAT]}
     (def add-VAT (fn [x] (* (+ 1 VAT) x))))))

And then we could use this module with different values of VAT:

(import (make-taxes 2/10))
;; We also need to import `prelude/io` to be able to use `print!`
(import (file-module! "prelude/io.rad") :unqualified)

(print! (string-append "100 EUR + VAT is: " (show (taxes/add-VAT 100))))

By default import will add all the definitions of the module in fully-qualified form, which means my-module/foo and my-module/bar (but not my-module/foo-helper) are in scope:

(print! (my-module/bar 0))

You can narrow down exactly which definitions you would like to import by including a list of atoms:

(import my-module ['foo])

Which would only import my-module/foo. And you can also use a custom qualifier:

(import my-module :as 'baz)

After which we can:

(print! (baz/bar 42))

So we could import our taxes module twive using different qualifiers:

(def taxes-fr (make-taxes 20/100))
(def taxes-de (make-taxes 19/100))

(import taxes-de :as 'de)
(import taxes-fr :as 'fr)

  "100 EUR + VAT is "
  (show (de/add-VAT 100))
  " EUR in Germany and "
  (show (fr/add-VAT 100))
  " EUR in France."))

If you really want to import all the definitions without a qualification, then you can use the keyword :unqualified like so:

(import my-module :unqualified)

And then we can:

(print! (foo 128))

Qualification and import lists can be combined like so:

(import my-module ['bar] :as 'useful)

Would only add useful/bar to the current scope:

(print! (useful/bar 0))

When working at the REPL, the function file-module! is also available, which can create a module from a file. It assumes the file starts with a module declaration, and is then equivalent to wrapping the contents of the file in (module ...).

Modules can be passed around and manipulated because they are just dicts with some metadata, and an :env key containing a radicle environment.