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 itType 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 itRead 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 itfn swap : (a, b) -> (b, a)
fn swap pair =
case pair of
(x, y) -> (y, x)
swap (1, "hello")
Try itAnonymous 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 itLambda 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 itMulti-line lambdas:
let compute = |x: Int|
let y = 3
x + y
compute 7
Try itLambda Patterns
Lambdas support irrefutable patterns (patterns that always match):
let addPair = |(x: Int, y: Int)| x + y
addPair (3, 4)
Try itAs-patterns (bind both the value and destructured parts):
let withAlias = |x as m: Int| (x, m)
withAlias 42
Try itTyped 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 itlet getName = |{ name: String, .. }| name
getName { name = "Alice", age = 30 }
Try itNote: 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 itHigher-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 itStandalone lambdas without context require explicit type annotations:
|x| x + 1
Try itlet f = |x: Int| x + 1
f 1
Try itUse lambdas with higher-order functions:
import List
[1, 2, 3] |> List.map (|x| x * 2)
Try itStdlib 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 itimport List
-- Lambda parameter type inferred from List.filter signature
List.filter (|x| x > 2) [1, 2, 3, 4, 5]
-- [3, 4, 5]
Try itFold 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 itMulti-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 itChained 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 itSupported 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 itCurrying 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 itmodule 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 itOverloading 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 itDifferent 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 itmodule 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 itOverloaded 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 itDuplicate 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 itRecursive 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 itmodule 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 itUsing case expressions:
fn factorial: Int -> Int
fn factorial n =
case n of
0 -> 1
1 -> 1
_ -> n * factorial (n - 1)
factorial 6 -- 720
Try itPattern 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 itList 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 itfn makeAdder : Int -> (Int -> Int)
fn makeAdder n = |x: Int| x + n
makeAdder 10 5
Try itCommon Higher-Order Functions
import List
-- map: transform each element
[1, 2, 3] |> List.map (|x| x * 2)
Try itimport List
-- filter: keep matching elements
[1, 2, 3, 4] |> List.filter (|x| x > 2)
Try itimport List
-- fold: reduce to single value
[1, 2, 3, 4] |> List.foldl (|acc x| acc + x) 0
Try itPipe 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 itimport List
[1, 2, 3, 4, 5]
|> List.filter (|x| x > 2)
|> List.map (|x| x * 2)
Try itNative 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 itAssigning 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 itFunction 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 itGeneric Functions
Functions can work with any type using type variables:
fn identity : a -> a
fn identity x = x
identity 42
Try itfn swap : (a, b) -> (b, a)
fn swap pair =
case pair of
(x, y) -> (y, x)
swap (1, "hello")
Try itfn 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 itLocal 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 itBest Practices
- Write type signatures for all top-level functions
- Keep functions small and focused on one task
- Use descriptive names —
calculateTaxnotcalc - Prefer pure functions without side effects
- Use case expressions for pattern matching
Next Steps
Learn about control flow for conditional logic.