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:

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

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

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

Module 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 it
module Math exposing (multiply)

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

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

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

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

  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 file inlining for composing programs from multiple files
  • Learn about error handling to understand Keel's helpful error messages