Modules
Modules help organize code into logical units and control visibility.
File-Level Modules
A file-level module declaration must appear at the start of the file. The module name is derived from the filename:
module exposing createUser, getName
fn createUser : String -> Int -> { name: String, age: Int }
fn createUser name age = { name = name, age = age }
fn getName : { name: String, age: Int } -> String
fn getName user = user.name
Exposing
Control what the module exports:
-- norun
-- expect-error: [Runtime] Parse error: Function does not exist inside the module; Function does not exist inside the module; Type does not exist inside the module; File-level module declaration must appear at the start of the file; File-level module declaration must appear at the start of the file
-- Expose specific items
module exposing functionA, functionB, TypeA
-- Expose all
module exposing ..
-- Expose nothing (private module)
module exposing ()
Named Inline Modules
Named inline modules must have their content indented:
module CustomMath exposing add, multiply
fn add : Int -> Int -> Int
fn add x y =
x + y
fn multiply : Int -> Int -> Int
fn multiply x y =
x * y
CustomMath.add 3 4
-- 7
Try itImportant: Non-indented content after an inline module declaration is an error:
-- Error: Inline module content must be indented
module CustomMath exposing add
fn add x y = x + y -- Wrong: not indented
Try itCorrect:
module CustomMath exposing add
fn add : Int -> Int -> Int
fn add x y =
x + y
CustomMath.add 5 3
-- 8
Try itRecursive Functions in Modules
Functions defined inside modules can call themselves recursively:
module CustomMath exposing factorial, fibonacci
fn factorial : Int -> Int
fn factorial n =
if n <= 1 then
1
else
n * factorial (n - 1)
fn fibonacci : Int -> Int
fn fibonacci n =
if n <= 1 then
n
else
fibonacci (n - 1) + fibonacci (n - 2)
CustomMath.factorial 5
-- 120
Try itmodule CustomMath exposing fibonacci
fn fibonacci : Int -> Int
fn fibonacci n =
if n <= 1 then
n
else
fibonacci (n - 1) + fibonacci (n - 2)
CustomMath.fibonacci 10
-- 55
Try itModule Access
Access module members with dot notation:
module CustomMath exposing add, multiply
fn add : Int -> Int -> Int
fn add x y =
x + y
fn multiply : Int -> Int -> Int
fn multiply x y =
x * y
CustomMath.add 1 2
-- 3
Try itmodule CustomMath exposing multiply
fn multiply : Int -> Int -> Int
fn multiply x y =
x * y
CustomMath.multiply 3 4
-- 12
Try itImports
Import modules to use their exports:
import Html
import Html.Attributes
Importing Standard Library Modules
Keel provides built-in modules that can be imported directly:
import Json
-- expect: Ok 6
import List
import Maybe
import Result
import String
List.map (|x : Int| x * 2) [1, 2, 3]
Json.encode { name = "Alice", age = 30 }
Ok 5 |> Result.map (|x : Int| x + 1)
Try itOr import specific functions directly:
import List exposing map, filter
map (|x : Int| x + 1) [1, 2, 3]
Try itSee the Standard Library for all available modules and functions.
Import with Alias
Create shorter aliases for modules using the as keyword:
import Html.Attributes as Attr
Aliases provide convenient shorthand throughout your code:
import List as L
-- tags: modules, imports, aliases
-- expect: 3
-- Modules and import aliases
import Math as M
M.abs (L.length [1, 2, 3])
Try itAliases work with the exposing clause:
import List as L exposing length
length [1, 2]
Try itNote: Aliases replace the original module name — after import Dir as D, only D is accessible; writing Dir.fn or Dir::Enum::Variant is a compile error. This applies to all access forms: M.fn, M::Enum::Variant, and M::Enum::Pattern. Aliases must be uppercase and are inherited by child scopes.
Import with Exposing
Import specific items directly:
import List exposing map
-- Using built-in list functions directly
[1, 2, 3] |> map (|x| x * 2) -- [2, 4, 6]
Try itUser-Defined File Modules
When you import a module that isn't part of the standard library, Keel looks for a matching .kl file in the modules/ directory relative to your entry file. Module names use PascalCase but filenames use snake_case:
my-project/
├── src/
│ └── main.kl
└── modules/
├── greeter.kl -- import Greeter
├── data_utils.kl -- import DataUtils
└── data/
├── list.kl -- import Data.List
└── nested/
└── deep.kl -- import Data.Nested.Deep
Defining a File Module
Each module file needs a module declaration with an exposing clause:
-- norun
module exposing double, square
fn double : Int -> Int
fn double x = x * 2
fn square : Int -> Int
fn square x = x * x
Importing File Modules
Import file modules the same way you import stdlib modules — all import forms work:
-- Qualified access
import Math
Math.double 5 -- 10
-- With alias
import Math as M
M.square 3 -- 9
-- Exposing specific functions
import Math exposing double
double 5 -- 10
-- Exposing everything
import Math exposing ..
square 4 -- 16
Nested Modules
Dots in the module name map to directory separators:
-- Imports modules/data/list.kl
import Data.List
Data.List.head [1, 2, 3]
-- Imports modules/data/nested/deep.kl
import Data.Nested.Deep
Modules Importing Other Modules
File modules can import both other user modules and stdlib modules:
-- norun
module exposing greeting
import String
fn greeting : String -> String
fn greeting name =
let parts = ["Hello", name]
String.join ", " parts
Symbol Resolution
When you import a module, Keel resolves symbol names through a specific order:
- Current scope — look for local definitions
- Parent scopes — walk up the scope chain
- Imported modules — check module exports
- Standard library — built-in modules like
List,Math
Module Exports
A module's exposing clause defines what symbols it exports:
module Utils exposing double, Triple, Point
fn double: Int -> Int
fn double x = x * 2
enum Triple = Triple { first : Int, second : Int, third : Int }
type Point = { x: Int, y: Int }
Try itdouble— Function exportedTriple— Enum exported (all constructors are always included)Point— Newtype exported
Import Resolution
Import statements determine which symbols are brought into scope:
-- Qualified: module name required
import List
List.length [1, 2] -- 2
-- Exposing: symbols directly accessible
import List exposing length, map
length [1, 2] -- 2
-- Alias: shorter qualified name
import List as L
L.length [1, 2] -- 2
Try itName Resolution
PascalCase module segments are converted to snake_case filenames:
| Module name | File path |
|---|---|
Greeter | modules/greeter.kl |
DataUtils | modules/data_utils.kl |
Data.List | modules/data/list.kl |
Data.Nested.Deep | modules/data/nested/deep.kl |
Circular Dependencies
Keel detects circular imports at compile time:
-- modules/cycle_a.kl
module exposing a
import CycleB -- Error: circular dependency
let a = CycleB.b
Example: Complete Module
import Math
module Geometry exposing Shape, area, perimeter
enum Shape
= Circle(Float)
| Rectangle(Float, Float)
| Triangle(Float, Float, Float)
fn area : Shape -> Float
fn area shape =
case shape of
Circle r -> 3.14159 * r * r
Rectangle w h -> w * h
Triangle a b c ->
let s = (a + b + c) / 2.0
let sq = s * (s - a) * (s - b) * (s - c)
case Math.sqrt sq of
Just v -> v
Nothing -> 0.0
fn perimeter : Shape -> Float
fn perimeter shape =
case shape of
Circle r -> 2.0 * 3.14159 * r
Rectangle w h -> 2.0 * (w + h)
Triangle a b c -> a + b + c
import Geometry exposing Shape, area
let circle = Shape::Circle 5.0
area circle
Try itmodule Geometry exposing perimeter
fn perimeter : Float -> Float
fn perimeter r =
2.0 * 3.14159 * r
Geometry.perimeter 5.0
-- 31.4159
Try itExporting and Importing Enums
Enums defined in a module can be exported and used in importing code:
module Shapes exposing Shape, area
enum Shape
= Circle(Float)
| Square(Float)
fn area : Shape -> Float
fn area shape =
case shape of
Circle r -> 3.14 * r * r
Square s -> s * s
import Shapes exposing Shape, area
let c = Shape::Circle 2.0
area c
Try itInclude the type name in the exposing list to make the enum and its constructors available to importers. Both exposing .. and specific exposing Type, fn syntax work.
Shadowing Standard Library Modules
User-defined modules can shadow stdlib module names if the stdlib version is not imported:
module Math exposing double
fn double : Int -> Int
fn double x =
x * 2
Math.double 5
Try itIf you import a stdlib module (List, String, Math, IO, Http, Json, Jsonl, DataFrame, Result, Maybe), you cannot define a module with the same name.
File Composition with Tasks
For composing a program from multiple files at compile time, Keel provides tasks. While modules define reusable namespaces with exports, tasks let you run another file's code and receive its exposed values as a record:
let x = 5
let { result } = run "compute.kl" { x }
result -- computed by compute.kl
The target file uses a task declaration to define its interface:
-- compute.kl
task expecting { x : Int } exposing { result : Int }
let result = x * 2
See the tasks guide for the full details on passing variables and record destructuring.
Best Practices
- One concept per module — keep modules focused
- Use descriptive names —
User.AuthenticationnotUA - Minimize exports — only expose what's needed
- Prefer qualified access — clearer where things come from
- Keep module content indented for inline modules
Next Steps
- Learn about tasks for composing programs from multiple files
- Learn about error handling to understand Keel's helpful error messages