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 (User, createUser, getName)
type alias User = { name: String, age: Int }
fn createUser : String -> Int -> User
fn createUser name age = { name = name, age = age }
fn getName : User -> String
fn getName user = user.name
Exposing
Control what the module exports:
-- 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 Math 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
Math.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 Math exposing (add)
fn add x y = x + y -- Wrong: not indented
Correct:
module Math exposing (add)
fn add : Int -> Int -> Int
fn add x y = x + y -- Correct: indented
Math.add 5 3 -- 8
Try itRecursive Functions in Modules
Functions defined inside modules can call themselves recursively:
module Math 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)
Math.factorial 5 -- 120
Try itmodule Math exposing (fibonacci)
fn fibonacci : Int -> Int
fn fibonacci n =
if n <= 1 then n
else fibonacci (n - 1) + fibonacci (n - 2)
Math.fibonacci 10 -- 55
Try itModule Access
Access module members with dot notation:
module Math 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
Math.add 1 2 -- 3
Try itmodule Math exposing (multiply)
fn multiply : Int -> Int -> Int
fn multiply x y = x * y
Math.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 List
import String
import Math
import IO
import Http
import Json
import DataFrame
import Result
import Maybe
-- Use qualified access
List.map (|x| x * 2) [1, 2, 3]
Json.encode { name = "Alice", age = 30 }
DataFrame.readCsv "data.csv"
Ok 5 |> Result.map (|x| x + 1) -- Ok 6
Just 5 |> Maybe.withDefault 0 -- 5
-- Or import specific functions
import List exposing (map, filter)
map (|x| x + 1) [1, 2, 3]
See 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 Math as M
M.abs (-5) -- 5
import List as L
L.map (|x: Int| x + 1) [1, 2, 3] -- [2, 3, 4]
Try itAliases work with the exposing clause:
import List as L exposing (length)
length [1, 2] -- 2 (direct access)
L.reverse [1, 2, 3] -- [3, 2, 1] (qualified access)
Try itNote: Aliases are additive — the original module name remains available. Aliases must be uppercase and are inherited by child scopes.
Import with Exposing
Import specific items directly:
-- Using built-in list functions directly
[1, 2, 3] |> map (|x| x * 2) -- [2, 4, 6]
Module Hierarchy
Organize related modules in directories:
modules/
├── trigonometry.kl -- Module: Trigonometry
├── optimization.kl -- Module: Optimization
├── math_lib.kl -- Module: MathLib
└── base/
├── main.kl -- Module: Base (entry point)
└── operators.kl -- Module: Base.Operators
Module names use PascalCase, but filenames use snake_case.
Example: Complete Module
module Geometry exposing (Shape, area, perimeter)
type 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
sqrt (s * (s - a) * (s - b) * (s - c))
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
let circle = Geometry.Circle 5.0
Geometry.area circle -- 78.54
module Geometry exposing (Shape, perimeter)
type Shape
= Circle(Float)
| Rectangle(Float, Float)
fn perimeter : Shape -> Float
fn perimeter shape = case shape of
Circle r -> 2.0 * 3.14159 * r
Rectangle w h -> 2.0 * (w + h)
let rect = Geometry.Rectangle 4.0 3.0
Geometry.perimeter rect -- 14.0
Exporting and Importing Enums
Enums defined in a module can be exported and used in importing code:
module Shapes exposing (Shape, area)
type 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 -- 12.56
Include 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 -- 10 (uses user's Math, not stdlib)
Try itIf you import a stdlib module (List, String, Math, IO, Http, Json, DataFrame, Result, Maybe), you cannot define a module with the same name.
File Composition with File Inlining
For composing a program from multiple files at compile time, Keel provides file inlining. While modules define reusable namespaces with exports, file inlining lets you inline another file's code and receive its exposed values as a record:
let x = 5
let { result } = inline "compute.kl" passing (x)
result -- computed by compute.kl
The inlined file uses a parameterized module declaration to define its interface:
-- compute.kl
module (x : Int) exposing (result : Int)
let result = x * 2
See the file inlining guide for the full details on passing, record destructuring, and mutation propagation.
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 file inlining for composing programs from multiple files
- Learn about error handling to understand Keel's helpful error messages