Esc
Start typing to search...

Functions

Functions are the primary building blocks in Keel. They are first-class values, meaning they can be passed as arguments, returned from other functions, and stored in variables.

Defining Functions

Functions are defined using the fn keyword with a type signature followed by the implementation:

fn greet: String -> String
fn greet name =
    "Hello, " ++ name ++ "!"

greet "Alice"  -- "Hello, Alice!"
Try it

Type Signatures

Type signatures declare the function's input and output types:

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

add 3 4  -- 7
Try it

Read Int -> Int -> Int as "takes an Int, returns a function that takes an Int and returns an Int" (currying).

Tuple Types as Arguments

Function signatures can use tuple types directly as arguments:

fn fst : (Int, String) -> Int
fn fst pair =
    case pair of
        (x, _) -> x

fst (42, "hello")  -- 42
fn swap : (a, b) -> (b, a)
fn swap pair =
    case pair of
        (x, y) -> (y, x)

swap (1, "hello")  -- ("hello", 1)

Anonymous Functions (Lambdas)

Create functions without names using pipe syntax:

let addOne = |x: Int| x + 1
let addTwo = |x: Int y: Int| x + y
let addTuple = |(x, y): (Int, Int)| x + y

addOne 5             -- 6

Lambda parameters can have type annotations:

let f1 = |x: Int| x + 1
let f2 = |x: Int y: Int| x + y
let f3 = |(x: Int, y: Int)| x + y
let f4 = |f: Int -> Int| f 1

f4 (|x: Int| x * 2)  -- 2

Multi-line lambdas:

let compute = |x: Int|
    let y = 3
    x + y

compute 7  -- 10

Lambda Patterns

Lambdas support irrefutable patterns (patterns that always match):

-- Tuple destructuring
let addPair = |(x, y): (Int, Int)| x + y

-- As-patterns (bind both the value and destructured parts)
let withAlias = |x as m: Int| (x, m)

addPair (3, 4)  -- 7

Typed Record Fields in Lambdas

Record fields in lambda patterns can have type annotations. This works standalone — no pipe context needed:

let describe = |{ name: String, age: Int }| name
describe { name = "Alice", age = 30 }  -- "Alice"

-- Mixed typed and untyped fields (untyped inferred from context)
{ x = 1, y = 2 } |> |{ x: Int, y }| x + y  -- 3

-- With rest pattern
let getName = |{ name: String, .. }| name

Note: Lambdas only accept irrefutable patterns. Refutable patterns like |Just x|, |5|, or |[a, b]| are rejected at parse time with a clear error message.

Lambda Type Inference

Lambda parameter types can be inferred from context:

-- Pipe operator provides context from the left operand
5
    |> |x| x + 1
Try it

Higher-order function context:

module M exposing (..)

    fn apply: (Int -> Int) -> Int -> Int
    fn apply f x =
        f x

M.apply (|y| y + 1) 5

-- y inferred as Int, result: 6
Try it

Standalone lambdas without context require explicit type annotations:

|x| x + 1                -- Error: requires type annotation
|x: Int| x + 1           -- OK: explicit annotation provided

Use lambdas with higher-order functions:

[1, 2, 3] |> map (|x| x * 2)  -- [2, 4, 6]

Stdlib Higher-Order Function Inference

Lambda parameter types are automatically inferred when passed to stdlib higher-order functions. The compiler examines the higher-order function's type signature and unifies type variables with concrete types from the call site.

For example, List.map has the signature (a -> b) -> List a -> List b. When called with a List Int, the type variable a unifies with Int, so the lambda parameter x is inferred to be Int:

import List

-- Lambda parameter type inferred from List.map signature
List.map (|x| x * 2)

[1, 2, 3]

-- [2, 4, 6]
Try it
import List

-- Lambda parameter type inferred from List.filter signature
List.filter (|x| x > 2)

[ 1
, 2
, 3
, 4
, 5
]

-- [3, 4, 5]
Try it

Fold Functions

Fold functions infer both the accumulator and element types:

import List

-- Accumulator and element types inferred from List.foldl signature
-- foldl : (b -> a -> b) -> b -> List a -> b
-- With initial value 0 (Int) and List Int, acc : Int and x : Int
List.foldl (|acc x| acc + x)

0

[ 1
, 2
, 3
, 4
]

-- 10
Try it

Multi-Parameter Inference

Functions like zipWith that take multiple parameters infer all parameter types:

import List

-- Both parameters inferred from List.zipWith signature
-- zipWith : (a -> b -> c) -> List a -> List b -> List c
-- With List Int and List Int, both x : Int and y : Int
List.zipWith (|x y| x * y)

[1, 2, 3]

[4, 5, 6]

-- [4, 10, 18]
Try it

Chained Operations

Type inference flows through pipelines. Each lambda's parameter type is inferred from the result of the previous operation:

import List

-- Type inference flows through chained operations
-- Each lambda's parameter type is inferred from the previous result
[ 1
, 2
, 3
, 4
, 5
]
    |> List.filter (|x| x > 2)
    |> List.map (|x| x * 2)
    |> List.foldl (|acc x| acc + x) 0

-- acc : Int, x : Int

-- Result: 24
Try it

Supported stdlib functions: map, filter, foldl, foldr, zipWith, any, all, find, partition, sortBy

See also: Generic Functions for how type variables work in function signatures.

Currying

All functions in Keel are curried by default:

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

let add5 = add 5    -- Partial application
add5 3              -- 8

Currying enables powerful function composition patterns.

Function Overloading

Functions can have the same name but different type signatures. The correct function is selected at compile time based on argument types:

module Math exposing (..)

    fn double: Int -> Int
    fn double x =
        x * 2

    fn double: String -> String
    fn double s =
        s ++ s

Math.double 5

-- 10
module Math exposing (..)

    fn double: Int -> Int
    fn double x =
        x * 2

    fn double: String -> String
    fn double s =
        s ++ s

Math.double "hi"

-- "hihi"

Overloading also works at the top level:

fn inc: Int -> Int
fn inc x =
    x + 1

fn inc: Float -> Float
fn inc x =
    x + 1.0

inc 5  -- 6
Try it

Different Arity Overloading

Functions with the same name but different numbers of parameters are supported:

module Math exposing (..)

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

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

Math.add 5

-- 6 (uses single-arg overload)
module Math exposing (..)

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

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

Math.add 5 3

-- 8 (uses two-arg overload)

Overloaded Higher-Order Functions

When passing lambdas to overloaded higher-order functions, parameter types are automatically inferred:

module Apply exposing (..)

    fn apply: (Int -> Int) -> Int -> Int
    fn apply f x =
        f x

    fn apply: (String -> String) -> String -> String
    fn apply f s =
        f s

Apply.apply (|x| x + 1) 5

-- 6: x inferred as Int
Try it

Duplicate Signature Detection

Functions with the same name AND same type signature are rejected:

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

fn add : Int -> Int    -- Error: Function 'add' already has an overload with the same signature
fn add x = x + 2

Recursive Functions

Use if-then-else or case expressions for recursion:

fn factorial: Int -> Int
fn factorial n =
    if n <= 1 then
        1
    else
        n * factorial (n - 1)

factorial 5  -- 120
Try it
fn fibonacci: Int -> Int
fn fibonacci n =
    if n <= 1 then
        n
    else
        fibonacci (n - 1) + fibonacci (n - 2)

fibonacci 10  -- 55
Try it

Using case expressions:

fn factorial: Int -> Int
fn factorial n =
    case n of
        0 -> 1
        1 -> 1
        _ -> n * factorial (n - 1)

factorial 6  -- 720
Try it

Pattern Matching in Functions

Match on argument structure using case expressions:

fn length : [a] -> Int
fn length list =
    case list of
        [] -> 0
        x :: xs -> 1 + length xs

length [1, 2, 3, 4]  -- 4

List types can be written as List a or [a] — both are equivalent.

Higher-Order Functions

Functions that take or return functions:

-- Takes a function as argument
fn applyTwice: (Int -> Int) -> Int -> Int
fn applyTwice f x =
    f (f x)

applyTwice (|x: Int| x + 1) 5  -- 7
Try it
-- Returns a function
fn makeAdder : Int -> (Int -> Int)
fn makeAdder n = |x: Int| x + n

let add10 = makeAdder 10
add10 5  -- 15

Common Higher-Order Functions

-- map: transform each element
[1, 2, 3] |> map (|x| x * 2)  -- [2, 4, 6]
-- filter: keep matching elements
[1, 2, 3, 4] |> filter (|x| x > 2)  -- [3, 4]
-- fold: reduce to single value
[1, 2, 3, 4] |> fold (|acc x| acc + x) 0  -- 10

Pipe Operators

Chain function calls elegantly:

fn double: Int -> Int
fn double x =
    x * 2

fn addOne: Int -> Int
fn addOne x =
    x + 1
    -- Forward pipe

5
    |> double
    |> addOne  -- Same as: addOne (double 5) = 11
Try it
-- Example pipeline
[1, 2, 3, 4, 5]
    |> filter (|x| x > 2)
    |> map (|x| x * 2)
    -- [6, 8, 10]

Native Functions in Pipes

Standard library native functions work directly with pipes and composition:

import String
"hello" |> String.toUpper |> String.length  -- 5
"hello" |> String.slice 0 3 |> String.toUpper  -- "HEL"

import List
[1, 2, 3] |> List.reverse |> List.length  -- 3

import Math
-5 |> Math.abs  -- 5

Assigning Generic Results to Variables

Results of generic stdlib functions can be assigned to variables with proper type inference:

import List

let doubled = List.map (|x| x * 2) [1, 2, 3]  -- [2, 4, 6]

let result = [1, 2, 3, 4, 5]
    |> List.map (|x| x * 2)
    |> List.filter (|x| x > 4)  -- [6, 8, 10]

Function Composition

Compose functions into new functions:

fn double : Int -> Int
fn double x = x * 2

fn addOne : Int -> Int
fn addOne x = x + 1

-- Forward composition (left to right)
let transform = double >> addOne    -- First double, then addOne

transform 5  -- 11

Generic Functions

Functions can work with any type using type variables:

fn identity : a -> a
fn identity x = x

identity 42        -- 42
fn swap : (a, b) -> (b, a)
fn swap pair =
    case pair of
        (x, y) -> (y, x)

swap (1, "hello")  -- ("hello", 1)
fn compose : (b -> c) -> (a -> b) -> a -> c
fn compose g f x = g (f x)

let double = |x: Int| x * 2
let addOne = |x: Int| x + 1

compose double addOne 5  -- 12

Local Bindings

Define helper bindings within a function:

fn quadraticFormula : Float -> Float -> Float -> (Float, Float)
fn quadraticFormula a b c =
    let discriminant = b * b - 4.0 * a * c
    let sqrtDisc = sqrt discriminant
    let root1 = (0.0 - b + sqrtDisc) / (2.0 * a)
    let root2 = (0.0 - b - sqrtDisc) / (2.0 * a)
    (root1, root2)

quadraticFormula 1.0 0.0 (0.0 - 4.0)  -- (2.0, -2.0)

Best Practices

  1. Write type signatures for all top-level functions
  2. Keep functions small and focused on one task
  3. Use descriptive namescalculateTax not calc
  4. Prefer pure functions without side effects
  5. Use case expressions for pattern matching

Next Steps

Learn about control flow for conditional logic.