Arl provides a module system for organizing code into reusable, encapsulated units. This guide covers how to create modules, import them, and understand how the module system resolves paths.
Overview
Arl has three mechanisms for loading code:
-
load: Evaluates a file in the current environment (source-like; definitions and imports from the file are visible in the caller) -
run: Evaluates a file in an isolated child environment (definitions and imports in the file are not visible in the caller) -
import: Loads a module and binds it as a first-class value; use:referto bring exports into scope unqualified
The load Function
load is the simplest mechanism - it reads and evaluates
a file in a target environment (defaulting to the current
environment).
The run Function
run reads and evaluates a file in a child of
the current environment. Definitions and imports in that file stay in
the child environment and are not visible in the caller. Use
run when you want to execute a script without polluting the
current scope (e.g. running a one-off task or a file that should not
affect the caller’s bindings).
The module Special Form
The module special form defines a named module with
explicit exports.
Nameless Modules
If you omit the module name, Arl derives it from the source filename:
; File: math-utils.arl
(module (export square cube)
(define square (lambda (x) (* x x)))
(define cube (lambda (x) (* x x x))))
; Registers as "math-utils" (derived from filename)This is convenient for single-module files where the filename already conveys the module name.
Export Strategies
Explicit exports (recommended):
(module mymodule
(export func1 func2 var1)
(define func1 (lambda (x) (* x 2)))
(define func2 (lambda (x) (+ x 1)))
(define var1 42)
(define private-helper (lambda (x) (/ x 2)))) ; Not exportedExport all:
(module mymodule
(export-all)
(define func1 (lambda (x) (* x 2)))
(define func2 (lambda (x) (+ x 1)))
(define _helper (lambda (x) (/ x 2)))) ; Not exported: _ prefix = privateNames beginning with _ (underscore) are private by
convention and are excluded from export-all. This lets you
define internal helpers without accidentally exposing them. The
_ prefix convention applies only to
export-all; explicit (export ...) can still
export _-prefixed names if needed.
Re-export imported symbols:
; Facade module that bundles several modules
(module collections
(export-all :re-export)
(import list :refer :all)
(import dict :refer :all)
(import set :refer :all))
; Users can import everything from the facade
(import collections :refer :all)By default, export-all excludes imported symbols. The
:re-export modifier includes them, enabling facade modules
that re-package multiple modules under a single name.
The import Special Form
The import special form loads a module and makes its
exports available. (import X) binds the module environment
to the symbol X. Access exports via qualified syntax
(X/sym), or use :refer to bring specific names
into scope unqualified. Use :as to alias the module
binding.
Signature
(import name) ; bind module as `name` (qualified access only)
(import name :refer (sym1 sym2)) ; bind module + only these names unqualified
(import name :refer :all) ; bind module + all exports unqualified
(import name :as alias) ; bind module as `alias`
(import name :as alias :refer (sym1)) ; alias + selective refer
(import name :rename ((old new))) ; rename specific names
(import "path") ; import by file path-
Symbol (e.g.
control): treated as a module name. Resolution looks in the stdlib directory first, then the current working directory. -
String (e.g.
"lib/utils.arl"): treated as a file path. Only path-based resolution is used (no stdlib lookup). The path is normalized to absolute so that the same file imported with different path strings (e.g."inst/arl/control.arl"and"./inst/arl/control.arl") reuses the same loaded module.
Qualified Access
After importing, you can access module exports using the
/ syntax:
(import math)
(math/sin 1.0) ; qualified access
(import math :as m)
(m/sin 1.0) ; aliased qualified access
(import math :refer (sin cos))
(sin 1.0) ; unqualified (referred)
(math/cos 0.0) ; qualified still worksThe / syntax is reader sugar for the
module-ref builtin: math/inc parses as
(module-ref math inc).
Import Modifiers
Import modifiers let you control which names are imported and how they appear in the current scope. This avoids namespace pollution and name collisions.
:refer — control which names are
available unqualified:
(import list :refer (map filter reduce)) ; only these names unqualified
(import list :refer :all) ; all exports unqualified:as — alias the module binding:
:rename — rename specific referred
names:
:reload — force re-evaluation of the
module source:
Modifiers work with both symbol and string imports, and apply to both regular values and macros.
Path Resolution
-
(import name)(symbol): resolve by module name — stdlib (inst/arl/), then current directory. The file must register itself with(module name ...). -
(import "path")(string): resolve by path only — the string is a file path (existing file orpath.arl). No stdlib lookup. Relative paths are resolved from the directory of the file containing theimport, not from the current working directory. This means(import "helper.arl")insidelib/main.arllooks forlib/helper.arl. When no source file is known (e.g. at the REPL), relative paths fall back to CWD. The path is normalized to absolute for caching, so re-importing the same file with a different path string does not reload it.
Module Registration and Scoping
When you import a module:
- If the module isn’t already registered for this engine, Arl loads the file containing the module (into the engine’s shared module cache)
- The file must contain a
(module ...)form that registers itself -
importattaches the module’s exports into the current environment (the scope where you wrote(import ...)) - Subsequent
(import M)in any scope reuses the same module instance and attaches its exports into that scope
Import scoping: Each (import M) only
makes M’s exports visible in the environment where that form was
evaluated. Imports in one file are not visible in another file or in the
REPL unless that file (or the REPL) also runs (import M).
Modules are loaded once per engine and shared; only the set of
environments that “see” the exports depends on where you call
import.
Module Introspection
Arl provides several builtins for inspecting modules at runtime:
-
(module? x)— returns#tifxis a module environment -
(namespace? x)— returns#tifxis a namespace node (created by hierarchical imports) -
(module-exports mod)— returns the list of exported symbol names -
(module-name mod)— returns the canonical name string of a module -
(module-ref mod sym)— look upsymin modulemod; the desugared form ofmod/sym
Error Handling
; Module not found
(import non-existent-module)
; Error: Module not found: non-existent-module
; Module file doesn't register itself
; File: bad.arl containing just (define x 10)
(import bad)
; Error: Module 'bad' did not register itself
; Accessing unexported symbol
; File: restricted.arl
(module restricted
(export public-fn)
(define public-fn (lambda () "visible"))
(define private-fn (lambda () "hidden")))
(import restricted :refer :all)
public-fn ; Works
private-fn ; Error: object 'private-fn' not foundCreating User Modules
Basic Module Structure
Create a file mymodule.arl:
(module mymodule
(export greet farewell)
(define greet
(lambda (name)
(string-concat "Hello, " name "!")))
(define farewell
(lambda (name)
(string-concat "Goodbye, " name "!"))))Then import it:
Module with Private Helpers
; File: calculator.arl
(module calculator
(export add subtract)
; Private helper
(define validate-number
(lambda (x)
(if (number? x)
x
(error "Not a number"))))
; Public functions
(define add
(lambda (a b)
(+ (validate-number a)
(validate-number b))))
(define subtract
(lambda (a b)
(- (validate-number a)
(validate-number b)))))Nested Module Loading
Modules can load other modules:
; File: string-helpers.arl
(module string-helpers
(export upcase downcase)
(define upcase (lambda (s) (toupper s)))
(define downcase (lambda (s) (tolower s))))
; File: text-utils.arl
(module text-utils
(export format-name)
(import string-helpers :refer :all)
(define format-name
(lambda (first last)
(string-concat (upcase first) " " (upcase last)))))
; Usage:
(import text-utils :refer :all)
(format-name "john" "doe") ; => "JOHN DOE"Standard Library Modules
Arl’s standard library is organized into modules. The 10
prelude modules (logic, core, types, list, equality,
functional, control, sequences, binding, threading) are loaded
automatically when creating an engine, so their exports are available by
default. Non-prelude modules require explicit import.
The module structure is also relevant when writing your own modules
(which must explicitly import their dependencies) or when creating a
bare engine with Engine$new(load_prelude = FALSE):
; Non-prelude modules require explicit import:
(import math :refer :all) ; inc, dec, abs, floor, ceiling, round, etc.
(import looping :refer :all) ; until, do-list, loop, recur
(import sort :refer :all) ; sort, sort-by
(import strings :refer :all) ; str, string-join, string-split, etc.
(import io :refer :all) ; display, println, read-line
(import dict :refer :all) ; dict operations
(import set :refer :all) ; set operations
; Prelude modules are already loaded but can be explicitly imported
; in your own modules (which start with an empty scope):
(import control :refer :all) ; when, unless, cond, case, try/catch/finally
(import binding :refer :all) ; let, let*, letrec
(import functional :refer :all) ; map, filter, reduce, etc.Core functions (car, cdr,
cons, list, arithmetic, predicates) come from
the R runtime layer; the Arl modules above add macros and higher-level
functions. To create an engine without prelude modules, use
Engine$new(load_prelude = FALSE) — builtins like
gensym, eval, read, and
cons are still available.
Best Practices
1. Use Explicit Exports
Prefer (export symbol1 symbol2 ...) over
(export-all) to maintain clear module interfaces:
Differences from Other Lisps
vs. Common Lisp
- Arl uses first-class module environments with qualified access
(
mod/sym) - No
use-package— use:referto bring names into scope - Simpler, file-based module system
Summary
-
load: Evaluates a file in the current environment; always re-evaluates (no caching, no stdlib lookup) -
run: Evaluates a file in an isolated child environment (definitions and imports not visible in the caller) -
module: Defines a module with explicit exports;(export-all)exports only symbols defined in the module (excluding_-prefixed private names and imported symbols) -
import: Loads a module, binds it as a first-class value, and optionally brings exports into scope via:refer; each module is loaded once per engine; qualified access viamod/sym - Circular imports are detected with a clear error message
- Use explicit exports for maintainable module interfaces