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

-- 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 List
import String
import Result
import Maybe
import Json

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:

-- Modules and import aliases
import Math as M

import List as L

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

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:

-- modules/math.kl
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:

-- modules/uses_stdlib.kl
module exposing (greeting)

import List

fn greeting : String -> String
fn greeting name =
    let parts = ["Hello", name]
    List.join ", " parts

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)

    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
            Math.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

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)
    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
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, 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 } = Task.run "compute.kl" (x)
result  -- computed by compute.kl

The target file uses a Task.define declaration to define its interface:

-- compute.kl
Task.define (x : Int) -> (result : Int)
let result = x * 2

See the tasks guide for the full details on passing variables, record destructuring, and mutation propagation.

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