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")
Try it
fn swap : (a, b) -> (b, a)
fn swap pair =
    case pair of
        (x, y) -> (y, x)

swap (1, "hello")
Try it

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: Int, y: Int)| x + y

addOne 5
Try it

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

Multi-line lambdas:

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

compute 7
Try it

Lambda Patterns

Lambdas support irrefutable patterns (patterns that always match):

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

addPair (3, 4)
Try it

As-patterns (bind both the value and destructured parts):

let withAlias = |x as m: Int| (x, m)
withAlias 42
Try it

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 }
Try it
-- Mixed typed and untyped fields (untyped inferred from context)
{ x = 1, y = 2 } |> |{ x: Int, y }| x + y
Try it
let getName = |{ name: String, .. }| name
getName { name = "Alice", age = 30 }
Try it

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
Try it
let f = |x: Int| x + 1
f 1
Try it

Use lambdas with higher-order functions:

import List

[1, 2, 3] |> List.map (|x| x * 2)
Try it

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

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 CustomMath exposing (..)

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

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

CustomMath.double 5

-- 10
Try it
module CustomMath exposing (..)

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

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

CustomMath.double "hi"

-- "hihi"
Try it

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 CustomMath exposing (..)

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

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

CustomMath.add 5

-- 6 (uses single-arg overload)
Try it
module CustomMath exposing (..)

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

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

CustomMath.add 5 3

-- 8 (uses two-arg overload)
Try it

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
fn add x = x + 2
Try it

Recursive Functions

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

module CustomMath exposing (factorial)

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

CustomMath.factorial 5
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
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]
Try it

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
fn makeAdder : Int -> (Int -> Int)
fn makeAdder n = |x: Int| x + n

makeAdder 10 5
Try it

Common Higher-Order Functions

import List

-- map: transform each element
[1, 2, 3] |> List.map (|x| x * 2)
Try it
import List

-- filter: keep matching elements
[1, 2, 3, 4] |> List.filter (|x| x > 2)
Try it
import List

-- fold: reduce to single value
[1, 2, 3, 4] |> List.foldl (|acc x| acc + x) 0
Try it

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

[1, 2, 3, 4, 5]
    |> List.filter (|x| x > 2)
    |> List.map (|x| x * 2)
Try it

Native Functions in Pipes

Standard library native functions work directly with pipes and composition:

import String
import List
import Math

let a = "hello" |> String.toUpper |> String.length
let b = "hello" |> String.slice 0 3 |> String.toUpper
let c = [1, 2, 3] |> List.reverse |> List.length
let d = 0 - 5
d |> Math.abs
Try it

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]

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

result
Try it

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

let transform = double >> addOne

transform 5
Try it

Generic Functions

Functions can work with any type using type variables:

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

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

swap (1, "hello")
Try it
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
Try it

Local Bindings

Define helper bindings within a function:

import Math

fn quadraticFormula : Float -> Float -> Float -> (Float, Float)
fn quadraticFormula a b c =
    let discriminant = b * b - 4.0 * a * c
    let sqrtDisc = Math.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)
Try it

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.