Skip to content

Chapter 13: Functional Programming

Tova's standard library includes a set of powerful functional programming utilities that let you build complex behavior by composing simple functions. These tools — compose, curry, partial, memoize, and others — aren't just academic exercises. They're practical tools for building reusable, testable code.

By the end of this chapter, you'll build a composable validation pipeline.

compose and pipe_fn

These two functions let you build new functions by chaining existing ones together.

compose — Right to Left

compose(f, g) creates a new function that applies g first, then f:

tova
fn add_one(x) { x + 1 }
fn double(x) { x * 2 }

add_then_double = compose(double, add_one)
print(add_then_double(3))   // double(add_one(3)) = double(4) = 8

compose reads like mathematical notation: compose(f, g)(x) = f(g(x)). You can compose any number of functions:

tova
fn negate_num(x) { 0 - x }

transform = compose(negate_num, double, add_one)
print(transform(3))   // negate(double(add_one(3))) = negate(8) = -8

pipe_fn — Left to Right

pipe_fn does the same thing but in the order you read:

tova
pipeline = pipeFn(add_one, double, negate_num)
print(pipeline(3))   // same result: -8

pipe_fn is often more intuitive because the data flows left-to-right, matching how you read code. Use whichever feels more natural.

tova
// Build a text normalizer by piping string operations
normalize = pipeFn(
  fn(s) trim(s),
  fn(s) lower(s),
  fn(s) replace(s, " ", "_")
)

print(normalize("  Hello World  "))   // "hello_world"
Try "compose and pipe_fn" in Playground

compose vs. pipe_fn vs. |>

The |> pipe operator works on values: value |> fn1() |> fn2(). compose and pipe_fn work on functions: they create a new function without calling it. Use |> when you have data to process now. Use compose/pipe_fn when you're building a reusable transformation to apply later.

curry — One Argument at a Time

curry transforms a function that takes multiple arguments into a series of functions that each take one:

tova
fn add(a, b) { a + b }

curried = curry(add)
add_five = curried(5)

print(add_five(3))    // 8
print(add_five(10))   // 15

Each call returns a new function until all arguments are provided:

tova
fn volume(l, w, h) { l * w * h }

curried_vol = curry(volume)
print(curried_vol(2)(3)(4))   // 24

// Create specialized functions
boxes_2m_wide = curried_vol(2)
boxes_2x3 = boxes_2m_wide(3)
print(boxes_2x3(5))   // 30

Currying with Collection Operations

Currying shines when combined with map, filter, and other higher-order functions:

tova
fn multiply(a, b) { a * b }

times = curry(multiply)
double = times(2)
triple = times(3)

numbers = [1, 2, 3, 4, 5]
print(numbers |> map(double))   // [2, 4, 6, 8, 10]
print(numbers |> map(triple))   // [3, 6, 9, 12, 15]
tova
fn greater_than(threshold, value) { value > threshold }

above = curry(greater_than)
above_10 = above(10)
above_50 = above(50)

scores = [5, 12, 48, 73, 8, 55, 91]
print(scores |> filter(above_10))   // [12, 48, 73, 55, 91]
print(scores |> filter(above_50))   // [73, 55, 91]
Try "curry and partial" in Playground

partial — Fix Some Arguments

partial is similar to currying but fixes specific arguments upfront and returns a function that takes the rest:

tova
fn log_message(level, category, msg) {
  print("[{level}] ({category}) {msg}")
}

// Fix the first argument
info = partial(log_message, "INFO")
error = partial(log_message, "ERROR")

info("auth", "User logged in")
// [INFO] (auth) User logged in

error("db", "Connection failed")
// [ERROR] (db) Connection failed

// Fix two arguments
auth_info = partial(log_message, "INFO", "auth")
auth_info("Session started")
// [INFO] (auth) Session started

curry vs. partial

Featurecurrypartial
Arguments appliedOne at a timeAny number at once
ReturnsCurried function chainSingle partially-applied function
Best forCreating families of functionsFixing known arguments
tova
// curry: create a family of comparators
fn clamp(lo, hi, value) { max(lo, min(hi, value)) }

bounded = curry(clamp)
percent = bounded(0)(100)       // clamp to 0-100
byte_val = bounded(0)(255)      // clamp to 0-255

print(percent(150))   // 100
print(byte_val(-5))   // 0

// partial: fix known context
fn send_email(from, to, subject, body) {
  print("From: {from} | To: {to} | {subject}")
}

send_from_system = partial(send_email, "system@app.com")
send_from_system("user@test.com", "Welcome", "Hello!")

memoize — Cache Results

memoize wraps a function so that repeated calls with the same arguments return a cached result instead of recomputing:

tova
expensive = memoize(fn(n) {
  print("Computing for {n}...")
  n * n * n
})

print(expensive(5))   // "Computing for 5..." then 125
print(expensive(5))   // 125 (no computation — cached)
print(expensive(3))   // "Computing for 3..." then 27
print(expensive(5))   // 125 (still cached)

Memoized Recursion

Memoization transforms exponential-time recursion into linear time:

tova
// Without memoize: O(2^n) — unusably slow for n > 30
fn slow_fib(n) {
  if n <= 1 { n }
  else { slow_fib(n - 1) + slow_fib(n - 2) }
}

// With memoize: O(n) — instant even for large n
fast_fib = memoize(fn(n) {
  if n <= 1 { n }
  else { fast_fib(n - 1) + fast_fib(n - 2) }
})

print(fast_fib(50))   // 12586269025 (instant)
Try "memoize" in Playground

Memoize Caveats

memoize uses argument serialization for cache keys. It works well for primitives (numbers, strings, booleans) but may not behave as expected for complex objects. Also, the cache grows indefinitely — don't memoize functions with unbounded input ranges in long-running processes.

once — Run Exactly Once

once ensures a function executes only on its first call. Subsequent calls return the first result:

tova
init_database = once(fn() {
  print("Connecting to database...")
  { connection: "db://localhost", status: "connected" }
})

// First call: runs the function
db = init_database()
print(db.status)   // "connected"

// Second call: returns cached result, no re-execution
db2 = init_database()
print(db2.status)  // "connected" (same object, no reconnection)

Use once for:

  • Initialization that should happen exactly once
  • Expensive setup (database connections, config loading)
  • Singleton patterns without global mutable state

negate — Flip a Predicate

negate takes a predicate function and returns one that returns the opposite boolean:

tova
is_even = fn(x) x % 2 == 0
is_odd = negate(is_even)

numbers = [1, 2, 3, 4, 5, 6, 7, 8]
print(numbers |> filter(is_even))   // [2, 4, 6, 8]
print(numbers |> filter(is_odd))    // [1, 3, 5, 7]

This is cleaner than writing fn(x) !is_even(x) and composes well with other functional utilities:

tova
is_empty_str = fn(s) len(trim(s)) == 0
has_content = negate(is_empty_str)

inputs = ["hello", "", "  ", "world", "   ", "tova"]
valid = inputs |> filter(has_content)
print(valid)   // ["hello", "world", "tova"]

flip — Swap Arguments

flip takes a function and returns a new one with the first two arguments swapped:

tova
fn divide(a, b) { a / b }

// Normal: divide(10, 2) = 5
print(divide(10, 2))

// Flipped: divide(2, 10) = 0.2
flipped_divide = flip(divide)
print(flipped_divide(10, 2))

flip is useful when you want to partially apply the second argument:

tova
fn starts_with_check(prefix, text) { startsWith(text, prefix) }

// We want to fix the text, not the prefix
check_greeting = partial(flip(starts_with_check), "Hello World")
print(check_greeting("Hello"))   // true
print(check_greeting("Bye"))     // false

identity — The Do-Nothing Function

identity returns its argument unchanged. This seems useless, but it's surprisingly handy:

tova
print(identity(42))        // 42
print(identity("hello"))   // "hello"

Practical Uses of identity

Filter truthy values:

tova
values = [0, "", nil, "hello", 42, false, true]
truthy = values |> filter(identity)
print(truthy)   // ["hello", 42, true]

Default transformation:

tova
fn process(items, transform) {
  transform_fn = transform ?? identity
  items |> map(transform_fn)
}

// No transform — identity passes values through
print(process([1, 2, 3], nil))           // [1, 2, 3]
print(process([1, 2, 3], fn(x) x * 2))  // [2, 4, 6]

Conditional pipeline steps:

tova
fn build_pipeline(options) {
  pipeFn(
    fn(s) trim(s),
    if options.lowercase { fn(s) lower(s) } else { identity },
    if options.truncate { fn(s) substr(s, 0, 10) } else { identity }
  )
}

clean = build_pipeline({ lowercase: true, truncate: false })
print(clean("  HELLO WORLD  "))   // "hello world"
Try "once, negate, flip, identity" in Playground

debounce — Wait for Calm

debounce(fn, ms) creates a function that delays execution until ms milliseconds have passed since the last call. If called again before the delay expires, the timer resets:

tova
save_draft = debounce(fn(text) {
  print("Saving: {text}")
}, 1000)

// User types rapidly
save_draft("H")
save_draft("He")
save_draft("Hel")
save_draft("Hell")
save_draft("Hello")
// Only "Hello" is saved — after 1 second of no typing

Use debounce for:

  • Search-as-you-type (wait until the user stops typing)
  • Window resize handlers (recalculate only after resizing stops)
  • Auto-save (save after a pause in editing)

throttle — Limit Frequency

throttle(fn, ms) creates a function that executes at most once every ms milliseconds. Calls during the cooldown period are ignored:

tova
report_scroll = throttle(fn(position) {
  print("Scroll position: {position}")
}, 200)

// Even if scroll fires 60 times per second,
// this logs at most every 200ms

Use throttle for:

  • Scroll and mouse move handlers (limit processing frequency)
  • Rate-limited API calls (respect API rate limits)
  • Progress reporting (update UI at a reasonable frequency)
Try "debounce and throttle" in Playground

debounce vs. throttle

Behaviordebouncethrottle
When it firesAfter ms of silenceAt most every ms
During rapid callsKeeps resetting timerFires on first, ignores rest until cooldown
Best for"Wait until done""Limit frequency"
ExampleSearch inputScroll handler

Combining Functional Utilities

The real power of these tools emerges when you combine them:

tova
// Build a robust API client with functional composition
fn make_api(base_url) {
  // Cache responses
  cached_fetch = memoize(fn(endpoint) {
    print("Fetching {base_url}{endpoint}...")
    { data: "response from {endpoint}" }
  })

  // Throttle requests to respect rate limits
  throttled_fetch = throttle(cached_fetch, 1000)

  // Return a clean interface
  {
    get: fn(endpoint) throttled_fetch(endpoint),
    url: base_url
  }
}

api = make_api("https://api.example.com")
tova
// Build a data processing pipeline with reusable transforms
parse_number = pipeFn(
  fn(s) trim(s),
  fn(s) replace(s, ",", ""),
  fn(s) toFloat(s)
)

format_currency = pipeFn(
  fn(n) round(n * 100) / 100,
  fn(n) toString(n),
  fn(s) "$" ++ s
)

process_price = pipeFn(parse_number, format_currency)

prices = ["  1,234.5 ", "99.999", " 42 "]
print(prices |> map(process_price))
// ["$1234.5", "$100.0", "$42.0"]

Project: Validation Pipeline Builder

Let's build a composable validation system using functional programming:

tova
// Base validators return Ok(value) or Err(message)
fn required(value) {
  if value == nil or value == "" {
    Err("Value is required")
  } else {
    Ok(value)
  }
}

fn min_length(n) {
  fn(value) {
    if len(toString(value)) < n {
      Err("Must be at least {n} characters")
    } else {
      Ok(value)
    }
  }
}

fn max_length(n) {
  fn(value) {
    if len(toString(value)) > n {
      Err("Must be at most {n} characters")
    } else {
      Ok(value)
    }
  }
}

fn matches_pattern(pattern, message) {
  fn(value) {
    if regexTest(pattern, toString(value)) {
      Ok(value)
    } else {
      Err(message)
    }
  }
}

// Compose validators: run each in sequence, stop on first error
fn validate_all(...validators) {
  fn(value) {
    var current = Ok(value)
    for v in validators {
      match current {
        Ok(val) => { current = v(val) }
        Err(_) => { return current }
      }
    }
    current
  }
}

// Build specific validators by composing primitives
validate_username = validate_all(
  required,
  min_length(3),
  max_length(20),
  matches_pattern(r"^[a-zA-Z0-9_]+$", "Only letters, numbers, and underscores")
)

validate_email = validate_all(
  required,
  min_length(5),
  matches_pattern(r"@", "Must contain @")
)

// Test
tests = ["", "ab", "alice_123", "has spaces!", "valid_user"]
for t in tests {
  result = validate_username(t)
  status = match result {
    Ok(_) => "PASS"
    Err(msg) => "FAIL: {msg}"
  }
  print("{padEnd(t, 15)} {status}")
}

The key insight: each validator is a function. validate_all composes them. min_length(3) and max_length(20) are factory functions — they use closures to capture configuration and return validators. This is functional programming at its most practical.

Try "Validation Pipeline" in Playground

Exercises

Exercise 13.1: Write a retry_fn(f, n) function using compose or pipe_fn that creates a function which tries f() up to n times, returning the first Ok result or the last Err. Don't use a loop — use recursion with a counter closure.

Exercise 13.2: Use curry to create a family of string formatting functions: pad_to(width, char, text). Then create pad_to_20 = curry(pad_to)(20)(" ") and use it with map to align a list of strings.

Exercise 13.3: Build a middleware combinator. Given an array of functions [fn(x) -> x, fn(x) -> x, ...], compose them into a single function that runs each in sequence, passing the result of one to the next. Then build a request processing pipeline: log -> authenticate -> validate -> handle.

Exercise 13.4: Create a memoized factorize(n) function that returns the prime factors of a number. Use memoize to cache results so that factorize(12) benefits from already having computed factorize(6) and factorize(4).

Challenge

Build a function testing framework using functional programming:

  1. describe(name, ...tests) — groups tests under a label
  2. it(name, fn) — defines a single test case
  3. expect(value) — returns an object with .toBe(x), .toContain(x), .toThrow()
  4. All assertions should use ResultOk for pass, Err for failure
  5. The runner should compose all test results and print a summary

Use compose, partial, and pipe_fn to build the assertion chain. Use once to ensure setup functions run exactly once. Use memoize to cache test fixtures.


← Previous: Capstone: Text Analyzer | Next: Standard Library Mastery →

Released under the MIT License.