Esc
Start typing to search...

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 it

Important: 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 it

Correct:

module CustomMath exposing add

    fn add : Int -> Int -> Int
    fn add x y =
        x + y

CustomMath.add 5 3

-- 8
Try it

Recursive 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 it
module 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 it

Module 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 it
module CustomMath exposing multiply

    fn multiply : Int -> Int -> Int
    fn multiply x y =
        x * y

CustomMath.multiply 3 4

-- 12
Try it

Imports

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 it

Or import specific functions directly:

import List exposing map, filter

map (|x : Int| x + 1) [1, 2, 3]
Try it

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 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 it

Aliases work with the exposing clause:

import List as L exposing length

length [1, 2]
Try it

Note: 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 it

User-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:

  1. Current scope — look for local definitions
  2. Parent scopes — walk up the scope chain
  3. Imported modules — check module exports
  4. 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 it
  • double — Function exported
  • Triple — 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 it

Name Resolution

PascalCase module segments are converted to snake_case filenames:

Module nameFile path
Greetermodules/greeter.kl
DataUtilsmodules/data_utils.kl
Data.Listmodules/data/list.kl
Data.Nested.Deepmodules/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 it
module Geometry exposing perimeter

    fn perimeter : Float -> Float
    fn perimeter r =
        2.0 * 3.14159 * r

Geometry.perimeter 5.0

-- 31.4159
Try it

Exporting 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 it

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
Try it

If 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

  1. One concept per module — keep modules focused
  2. Use descriptive namesUser.Authentication not UA
  3. Minimize exports — only expose what's needed
  4. Prefer qualified access — clearer where things come from
  5. 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