Modules & Imports¶
Endo's module system lets you organize functions, types, and values into reusable, namespaced units. Modules follow F# conventions: PascalCase names, qualified access, and import-once semantics.
12.1 Inline Module Definitions¶
Modules can be defined inline using module Name = ...:
module Helpers =
let double (x: int) : int = x * 2
let triple (x: int) : int = x * 3
println (Helpers.double 5) # => 10
println (Helpers.triple 4) # => 12
All bindings inside the module are public and accessible via qualified access (Module.function). Inline modules persist across REPL prompts.
12.2 Multiple Inline Modules¶
You can define several modules and call across them freely:
module Math =
let square (x: int) : int = x * x
let cube (x: int) : int = x * x * x
module Utils =
let inc (x: int) : int = x + 1
let dec (x: int) : int = x - 1
println (Math.square 5) # => 25
println (Math.cube 3) # => 27
println (Utils.inc (Math.square 4)) # => 17
12.3 Opening Modules¶
open loads a module and brings all its public names into the current scope. Both qualified and unqualified access work after open.
module Math =
let square (x: int) : int = x * x
let cube (x: int) : int = x * x * x
open Math
println (square 5) # => 25
println (cube 3) # => 27
println (Math.square 4) # => 16
Selective Open¶
open Module with (name1, name2, ...) imports only the listed names into unqualified scope. Qualified access to all public members still works.
module Math =
let square (x: int) : int = x * x
let cube (x: int) : int = x * x * x
let double (x: int) : int = x * 2
open Math with (square, cube)
println (square 5) # => 25
println (cube 3) # => 27
println (Math.double 4) # => 8
12.4 File-Based Modules¶
A .endo file is a module. The filename determines the module name (PascalCase convention). No module declaration is needed inside the file -- the filename is the module name.
For example, a file named Math.endo with the following content defines module Math:
You can then load it with import Math and use Math.square 5.
Nested modules use directory structure. For import Geometry.Circle, Endo looks for:
12.5 Importing Modules¶
import loads a file-based module and makes its members available via qualified access. Members are not added to the unqualified scope -- you must always use the Module.name prefix.
open can then bring names into scope (same as with inline modules):
12.6 Shell Keyword Harmony¶
import, open, and module are not reserved keywords. They are only recognized as module directives when followed by a PascalCase identifier (starting with an uppercase letter). This means common shell commands continue to work unchanged:
The line above is treated as a shell command (because requests is lowercase), not a module import. Similarly:
This is a shell command, not an inline module definition (because load is lowercase).
The following, however, defines an inline module (because Helpers is PascalCase):
12.7 Import-Once Semantics¶
Each module is compiled and its top-level bindings evaluated exactly once per session, regardless of how many times it is imported or opened.
The module cache is keyed by resolved absolute file path, so the same physical file loaded via different relative paths is still recognized as one module.
12.8 Module Resolution¶
When resolving import Foo, Endo searches these locations in order:
| Priority | Location | Example |
|---|---|---|
| 1 | Relative to importing file | ./Foo.endo |
| 2 | Each search path in order | ~/.config/endo/modules/Foo.endo, <prefix>/share/endo/stdlib/Foo.endo |
For nested modules like import Geometry.Circle:
| Priority | Location |
|---|---|
| 1 | ./Geometry/Circle.endo |
| 2 | ~/.config/endo/modules/Geometry/Circle.endo |
| 3 | <prefix>/share/endo/stdlib/Geometry/Circle.endo |
12.9 REPL Usage¶
Modules work interactively in the REPL. Imported modules, opened names, and inline module definitions all persist across prompts:
12.10 Circular Dependency Detection¶
Endo detects circular dependencies at load time. If module A imports module B, which imports module A, a clear error is reported:
12.11 Error Messages¶
| Situation | Error Message |
|---|---|
| Module not found | Module 'Foo' not found. Searched: ./Foo.endo, ... |
| Circular dependency | Circular module dependency: A → B → A |
| Unknown member | Module 'Math' has no member 'nonexistent' |
| Private member | 'Math.helper' is private and cannot be accessed outside module 'Math' |
| No module loader | Module system not available (no module loader configured) |
| Selective open mismatch | Module 'Math' has no member 'nonexistent' (in selective open) |
12.12 Interaction with Built-in Modules¶
Built-in modules (File, Path, DateTime, Size, TimeSpan, etc.) take priority over user-defined modules of the same name. These are always available without an import statement:
User-defined modules require explicit import or inline module ... = definitions.
12.13 Running Scripts¶
Endo provides several ways to run .endo scripts:
| Method | Syntax | Context |
|---|---|---|
| CLI | endo script.endo | Runs in a new shell session |
| Shell mode | ./script.endo or path/to/script.endo | Executes in-process (current context) |
source | source script.endo | Shell builtin, current context |
run_script | run_script "script.endo" | F# callable, current context |
All methods set the source file path so that import statements within the script resolve relative to the script's location.
Additional module search paths can be added via the CLI:
12.14 Creating Reusable Modules¶
To create a reusable module, write a .endo file with a PascalCase name and place it in one of the module search paths (e.g., ~/.config/endo/modules/).
Here is a self-contained example using inline modules that demonstrates the same patterns you would use with file-based modules:
module StringUtils =
let rec repeat (acc: str) (s: str) (n: int) : str =
match n with
| 0 -> acc
| _ -> repeat (acc + s) s (n - 1)
let greet (name: str) : str = "Hello, " + name + "!"
println (StringUtils.repeat "" "ha" 3) # => hahaha
println (StringUtils.greet "world") # => Hello, world!
Or bring names into scope with open:
module StringUtils =
let rec repeat (acc: str) (s: str) (n: int) : str =
match n with
| 0 -> acc
| _ -> repeat (acc + s) s (n - 1)
open StringUtils with (repeat)
println (repeat "" "ho" 3) # => hohoho
12.14 Summary¶
| Feature | Syntax | Effect |
|---|---|---|
| Inline module | module M = ... | Define module in current file/REPL |
| Import (file) | import Math | Qualified access: Math.square 5 |
| Open | open Math | Unqualified + qualified access |
| Selective open | open Math with (square) | Only listed names unqualified |
| Nested import | import Geometry.Circle | Load from Geometry/Circle.endo |
See also: Functions | Standard Library | Error Handling