File Inlining
Keel supports composing programs from multiple files using inline "file" expressions. All file reading, parsing, and inlining happens at compile time — there are no runtime file operations.
Inlining a File
Use inline "path" to inline another file. The expression returns a record of exposed values, which you destructure with let:
let x = 5
let { result } = inline "compute.kl" passing (x)
result -- value computed by compute.kl
The compiler reads the target file, type-checks it, and inlines its code at that point. The inline expression returns a record whose fields are the callee's exposed variables.
The Callee Module Declaration
The inlined file declares its inputs and outputs with a parameterized module declaration:
-- compute.kl
module (x : Int) exposing (result : Int)
let result = x * 2
(x : Int, ...)declares typed input parameters — what the caller must passexposing (result : Int, ...)declares typed output variables — the fields of the returned record
Passing Variables
The passing clause supports three forms:
| Form | Meaning | Example |
|---|---|---|
(x, y) | Named variables | inline "f.kl" passing (x, y) |
(..) | All variables | inline "f.kl" passing (..) |
() | No variables | inline "f.kl" passing () |
The passing clause is optional when no variables need to be passed — inline "f.kl" is equivalent to inline "f.kl" passing ().
Receiving Values
The inline expression returns a record, so you use record destructuring to receive values:
-- Destructure specific fields
let { result } = inline "compute.kl" passing (x)
-- Destructure multiple fields
let { sum, product } = inline "math.kl" passing (a, b)
-- Ignore the result
let _ = inline "side_effect.kl"
Multiple Variables
Pass and receive multiple variables:
-- math.kl
module (a : Int, b : Int) exposing (sum : Int, product : Int)
let sum = a + b
let product = a * b
let a = 3
let b = 7
let { sum, product } = inline "math.kl" passing (a, b)
sum -- 10
product -- 21
Variable Flow
Variables move between files in a controlled, explicit way:
- The caller passes variables:
passing (x, y)orpassing (..) - The callee declares typed parameters:
module (x : Int, y : Int) - The callee declares typed outputs:
exposing (result : Int) - The caller destructures the returned record:
let { result } = inline ...
Variables not passed are invisible to the callee. Variables not in exposing are not returned. This keeps file boundaries explicit.
Mutation Propagation
To propagate a re-bound variable back to the caller, mark it mut in the callee's exposing clause:
-- double.kl
module (x : Int) exposing (result : Int, mut x : Int)
let x = x * 2
let result = x + 1
let x = 5
let { result, x } = inline "double.kl" passing (x)
x -- 10 (mutated because of 'mut' in exposing)
result -- 11
Without mut, re-binding a parameter inside the callee does not affect the caller's variable:
-- no_propagation.kl
module (x : Int) exposing (result : Int)
let x = x * 2 -- local change only
let result = x + 1
let x = 5
let { result } = inline "no_propagation.kl" passing (x)
x -- 5 (unchanged — no 'mut' in exposing)
result -- 11
The mut variable must be one of the module's input parameters — marking a locally created variable as mut is a compile error.
Expose-Only Files
A callee doesn't need input parameters. It can simply compute values and expose them:
-- constants.kl
module exposing (answer : Int)
let answer = 7 * 6
let { answer } = inline "constants.kl"
answer -- 42
Working with Strings
File inlining works with all Keel types:
-- greet.kl
module (name : String) exposing (greeting : String)
let greeting = "Hello, " ++ name
let name = "World"
let { greeting } = inline "greet.kl" passing (name)
greeting -- "Hello, World"
Sequential Inlines
You can inline multiple files in sequence. Each inline can use values from earlier inlines:
let x = 5
let { a } = inline "step1.kl" passing (x)
let { b } = inline "step2.kl" passing (x)
a + b
Chained Inlines
Inlined files can themselves inline other files, creating chains of composition:
-- inner.kl
module (n : Int) exposing (doubled : Int)
let doubled = n * 2
-- outer.kl
module (x : Int) exposing (final_result : Int)
let n = x
let { doubled } = inline "inner.kl" passing (n)
let final_result = doubled + x
let x = 5
let { final_result } = inline "outer.kl" passing (x)
final_result -- 15
File paths are resolved relative to the inlining file's directory, so nested files can use relative paths like "../sibling/file.kl".
Error Handling
The compiler catches file inlining errors at compile time:
| Error | Cause |
|---|---|
| File not found | The file path doesn't resolve to an existing file |
| Parse errors in inlined file | The referenced file has syntax errors |
| Circular inline dependency | File A inlines B, which inlines A (or a file inlines itself) |
| Missing expected variable | The callee expects a parameter the caller didn't pass |
| Extra variable | The caller passes a variable the callee doesn't expect |
| Type mismatch | A passed variable's type doesn't match the expected type |
| Mut variable not in params | A mut variable in exposing isn't one of the module's input parameters |
All errors are reported at compile time — there are no runtime surprises.
Best Practices
- Keep inlined files focused — each file should do one thing
- Be explicit about interfaces — use typed
moduledeclarations to document inputs and outputs - Prefer named variables —
passing (x)is clearer thanpassing (..) - Destructure only what you need —
let { result } = inline ...is clearer than binding everything - Avoid deep nesting — chained inlines are powerful, but deep chains become hard to follow
- Use descriptive file names —
compute_totals.klis clearer thanstep2.kl
Next Steps
Learn about error handling to understand Keel's helpful error messages, or explore the standard library for built-in functions.