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") -- 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 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 -- 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 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 -- 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 itDifferent 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 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 -- 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 itfn fibonacci: Int -> Int
fn fibonacci n =
if n <= 1 then
n
else
fibonacci (n - 1) + fibonacci (n - 2)
fibonacci 10 -- 55
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] -- 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
- 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.