Esc
Start typing to search...

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 pass
  • exposing (result : Int, ...) declares typed output variables — the fields of the returned record

Passing Variables

The passing clause supports three forms:

FormMeaningExample
(x, y)Named variablesinline "f.kl" passing (x, y)
(..)All variablesinline "f.kl" passing (..)
()No variablesinline "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:

  1. The caller passes variables: passing (x, y) or passing (..)
  2. The callee declares typed parameters: module (x : Int, y : Int)
  3. The callee declares typed outputs: exposing (result : Int)
  4. 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:

ErrorCause
File not foundThe file path doesn't resolve to an existing file
Parse errors in inlined fileThe referenced file has syntax errors
Circular inline dependencyFile A inlines B, which inlines A (or a file inlines itself)
Missing expected variableThe callee expects a parameter the caller didn't pass
Extra variableThe caller passes a variable the callee doesn't expect
Type mismatchA passed variable's type doesn't match the expected type
Mut variable not in paramsA 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

  1. Keep inlined files focused — each file should do one thing
  2. Be explicit about interfaces — use typed module declarations to document inputs and outputs
  3. Prefer named variablespassing (x) is clearer than passing (..)
  4. Destructure only what you needlet { result } = inline ... is clearer than binding everything
  5. Avoid deep nesting — chained inlines are powerful, but deep chains become hard to follow
  6. Use descriptive file namescompute_totals.kl is clearer than step2.kl

Next Steps

Learn about error handling to understand Keel's helpful error messages, or explore the standard library for built-in functions.