Skip to content

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:

kettle
-- Store a function in a variable
add_one = fn(x: Int) -> x + 1

-- Use it
result = add_one(5)  -- 6

Anonymous Functions ​

Create functions inline without naming them:

kettle
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:

kettle
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)))    -- 13

Higher-Order Functions ​

Functions that take or return other functions.

Map ​

Transform each element:

kettle
names = ["alice", "bob", "carol"]
lengths = names | map(fn(s: String) -> length(s))
-- [5, 3, 5]

Filter ​

Keep elements matching a predicate:

kettle
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:

kettle
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 ​

kettle
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 fn

Matching Tuples ​

kettle
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 fn

Matching on List Length ​

kettle
fn describe_list(lst: List[Int]) -> String
  match length(lst)
    0 -> "empty"
    1 -> "single element"
    2 -> "pair"
    _ -> "many elements"
  end match
end fn

Guards ​

Add conditions to patterns:

kettle
fn classify(n: Int) -> String
  match n
    0 -> "zero"
    n when n > 0 -> "positive"
    _ -> "negative"
  end match
end fn

Pipelines ​

The pipe operator | chains function calls:

kettle
-- 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:

kettle
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:

kettle
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 fn

Currying 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:

kettle
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 fn

This works with built-in higher-order functions:

kettle
-- 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:

kettle
-- 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)  -- 8

Single Placeholder ​

The most common case is a single placeholder:

kettle
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 fn

Multiple Placeholders ​

You can use multiple placeholders to create functions with multiple parameters:

kettle
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:

kettle
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 fn

Placeholders 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.
kettle
-- 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 open

Placeholders 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:

kettle
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 fn

The 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:

kettle
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 fn

Next Steps ​