Functional Programming β
Kettle is a functional language at heart. This chapter covers closures, higher-order functions, and pipelines.
First-Class Functions β
Functions are valuesβyou can store them in variables, pass them as arguments, and return them from other functions:
-- Store a function in a variable
add_one = fn(x: Int) -> x + 1
-- Use it
result = add_one(5) -- 6Anonymous Functions β
Create functions inline without naming them:
doubled = [1, 2, 3] | map(fn(x: Int) -> x * 2)
-- [2, 4, 6]
evens = [1, 2, 3, 4, 5] | filter(fn(x: Int) -> x % 2 == 0)
-- [2, 4]Closures β
Functions capture variables from their enclosing scope:
fn make_adder(n: Int) -> Fn(Int) -> Int
fn(x: Int) -> x + n
end fn
add_five = make_adder(5)
add_ten = make_adder(10)
print(to_string(add_five(3))) -- 8
print(to_string(add_ten(3))) -- 13Higher-Order Functions β
Functions that take or return other functions.
Map β
Transform each element:
names = ["alice", "bob", "carol"]
lengths = names | map(fn(s: String) -> length(s))
-- [5, 3, 5]Filter β
Keep elements matching a predicate:
numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
big = numbers | filter(fn(x: Int) -> x > 5)
-- [6, 7, 8, 9, 10]Fold β
Reduce a list to a single value:
numbers = [1, 2, 3, 4, 5]
-- Sum
total = numbers | fold(0, fn(acc: Int, x: Int) -> acc + x)
-- 15
-- Product
product = numbers | fold(1, fn(acc: Int, x: Int) -> acc * x)
-- 120
-- Build a string
joined = numbers | fold("", fn(acc: String, x: Int) -> "${acc}${to_string(x)} ")
-- "1 2 3 4 5 "Pattern Matching β
Match is powerful for destructuring data:
Matching Variants β
type MaybeInt = variant
Just(value: Int)
Nothing
end variant
fn unwrap_or(m: MaybeInt, default: Int) -> Int
match m
Just(v) -> v
Nothing -> default
end match
end fnMatching Tuples β
fn describe_point(p: Tuple[Int, Int]) -> String
match p
(0, 0) -> "origin"
(0, _) -> "on y-axis"
(_, 0) -> "on x-axis"
(x, y) -> "at (${to_string(x)}, ${to_string(y)})"
end match
end fnMatching on List Length β
fn describe_list(lst: List[Int]) -> String
match length(lst)
0 -> "empty"
1 -> "single element"
2 -> "pair"
_ -> "many elements"
end match
end fnGuards β
Add conditions to patterns:
fn classify(n: Int) -> String
match n
0 -> "zero"
n when n > 0 -> "positive"
_ -> "negative"
end match
end fnPipelines β
The pipe operator | chains function calls:
-- Without pipes (nested calls read inside-out)
doubled = map(fn(x: Int) -> x * 2, filter(fn(x: Int) -> x > 0, [1, -2, 3]))
-- With pipes (reads left-to-right)
doubled = [1, -2, 3] | filter(fn(x: Int) -> x > 0) | map(fn(x: Int) -> x * 2)Data flows left to right, making transformations easier to read:
numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
-- Chain transformations: keep evens, square them, sum them
result = numbers | filter(fn(x: Int) -> x % 2 == 0) | map(fn(x: Int) -> x * x) | sum
-- result = 220 (4 + 16 + 36 + 64 + 100)Function Composition β
Build new functions by combining existing ones:
fn compose_int(f: Fn(Int) -> Int, g: Fn(Int) -> Int) -> Fn(Int) -> Int
fn(x: Int) -> f(g(x))
end fn
fn main() -> Unit using file_io
double = fn(x: Int) -> x * 2
add_one = fn(x: Int) -> x + 1
double_then_add = compose_int(add_one, double)
add_then_double = compose_int(double, add_one)
print(to_string(double_then_add(5))) -- 11 (5*2 + 1)
print(to_string(add_then_double(5))) -- 12 ((5+1) * 2)
end fnCurrying and Partial Application β
Built-in functions like map, filter, and fold support automatic currying. When you call them with fewer arguments than expected, you get back a partially applied function:
fn main() -> Unit using file_io
-- filter expects 2 args: (predicate, list)
-- Calling with 1 arg returns a function
get_evens = filter(fn(x: Int) -> x % 2 == 0)
-- Now get_evens is a function: List[Int] -> List[Int]
result = get_evens([1, 2, 3, 4, 5])
print(result) -- [2, 4]
end fnThis works with built-in higher-order functions:
-- Partially apply filter with a predicate
get_evens = filter(fn(x: Int) -> x % 2 == 0)
-- Use it on different lists
evens1 = get_evens([1, 2, 3, 4]) -- [2, 4]
evens2 = get_evens([5, 6, 7, 8]) -- [6, 8]
-- Partially apply map
double = map(fn(x: Int) -> x * 2)
doubled = double([1, 2, 3]) -- [2, 4, 6]Placeholder Syntax β
Kettle provides a convenient _ placeholder for creating partial applications. When you use _ in a function call, it creates a new function with that argument position left open:
-- Using a placeholder to create a partial application
add = fn(a: Int, b: Int) -> a + b
add_five = add(5, _) -- Creates fn(b) -> add(5, b)
result = add_five(3) -- 8Single Placeholder β
The most common case is a single placeholder:
fn main() -> Unit using file_io
numbers = [1, 2, 3, 4, 5]
-- Create a function that multiplies by 2
double = mult(2, _)
doubled = numbers | map(double)
print(doubled) -- [2, 4, 6, 8, 10]
-- Placeholder in first position
bits = [0, 1, 0, 1, 1, 0, 1, 0]
get_bit = get_at(_, bits) -- fn(i) -> get_at(i, bits)
third = get_bit(2)
print(third) -- Some(0)
end fnMultiple Placeholders β
You can use multiple placeholders to create functions with multiple parameters:
fn greet(greeting: String, name: String, punctuation: String) -> String
"${greeting}, ${name}${punctuation}"
end fn
-- Create a function with two open positions
say_hello = greet("Hello", _, _)
-- say_hello has type: Fn(String, String) -> String
message = say_hello("Alice", "!") -- "Hello, Alice!"Placeholders with Pipelines β
Placeholders work naturally with the pipe operator, making it easy to create inline transformations:
fn main() -> Unit using file_io
data = [("Alice", 25), ("Bob", 30), ("Carol", 28)]
-- Extract ages and double them
doubled_ages = data | map(get_at(1, _)) | cat_options | map(mult(2, _))
print(doubled_ages) -- [50, 60, 56]
end fnPlaceholders vs Currying β
Both placeholders and currying create partial applications, but they serve different purposes:
- Currying: Works with built-in functions. Call with fewer arguments than expected to get a function waiting for the rest.
- Placeholders: Work with any function. Let you leave any argument position open, not just trailing ones.
-- Currying only works with builtins like filter, map, fold:
get_evens = filter(fn(x: Int) -> x % 2 == 0) -- Currying
-- Placeholders work with any function:
get_first = get_at(0, _) -- First arg fixed, second openPlaceholders are particularly useful when you want to fix an argument that isn't the first one, or when working with user-defined functions.
Pipe-Friendly Design β
Kettle builtins use data-last argument order, making them work naturally with the pipe operator:
fn main() -> Unit using file_io
-- Process a list through multiple transformations
result = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10] | filter(fn(x: Int) -> x % 2 == 0) | map(fn(x: Int) -> x * x) | sum
print(result) -- 220 (4 + 16 + 36 + 64 + 100)
end fnThe pipe operator | passes the left side as the last argument to the right side. Since data comes last in Kettle functions, this creates natural pipelines.
Why Data-Last? β
With data-last ordering, you can create reusable transformations by partial application:
fn main() -> Unit using file_io
-- Create reusable pipeline stages
get_positive = filter(fn(x: Int) -> x > 0)
double_all = map(fn(x: Int) -> x * 2)
numbers = [-3, -1, 4, 1, 5, 9, -2, 6]
result = numbers | get_positive | double_all
print(result) -- [8, 2, 10, 18, 12]
end fnNext Steps β
- Effects System β Managing side effects
- Linear Types β Resource safety