❄️ Tinsel AoC DSL

Lesson 03: Functions, Closures & Pipelines

In this lesson, you will learn how to structure reusable logic in Tinsel using named functions (fn), parameter destructuring, default arguments, anonymous lambdas (->), lexical closures, and the forward pipeline operator (|>).


1. Learning Objectives

By the end of this lesson, you will be able to:

  • Define named functions (fn) with destructured parameters and default parameter values.
  • Write concise arrow lambdas (x -> ...) and multi-statement block lambdas.
  • Capture enclosing state using lexical closures.
  • Chain data transformations left-to-right with the |> pipeline operator and _ placeholders.

2. Named Functions (fn), Destructuring & Default Arguments

Functions are declared with fn. A function body is a block expression whose final trailing expression is returned automatically (or you can exit early with return):

tinsel
fn clamp_val(x, low = 0, high = 100) {
    if x < low {
        return low
    }
    if x > high {
        high
    } else {
        x
    }
}

assert(clamp_val(50) == 50)
assert(clamp_val(-10) == 0)
assert(clamp_val(250, 0, 200) == 200)

Parameter Pattern Destructuring

Function parameters can destructure tuples, arrays, or structs directly in the signature:

tinsel
fn taxicab((x1, y1), (x2, y2) = (0, 0)) {
    abs(x1 - x2) + abs(y1 - y2)
}

assert(taxicab((3, 4), (1, -2)) == 8)
assert(taxicab((5, -12)) == 17)

3. Anonymous Lambdas (-> and fn(...))

Functions are first-class values in Tinsel. You can write anonymous functions using arrow syntax (->) or fn(...) { ... }:

tinsel
// Single parameter (parentheses optional)
let square = x -> x * x
assert(square(9) == 81)

// Multiple parameters with default value
let scale_add = (x, factor = 10) -> x * factor + 1
assert(scale_add(4) == 41 && scale_add(4, 2) == 9)

// Destructuring a tuple parameter inside a lambda (note the double parentheses)
let pairs = [(2, 3), (4, 5), (6, 7)]
let products = pairs.map(((a, b)) -> a * b)
assert(products == [6, 20, 42])

// Multi-statement block lambda
let parse_and_double = s -> {
    let n = s.trim().to_int()
    n * 2
}
assert(parse_and_double("  35 ") == 70)
Tip

You can inspect the number of parameters a function or closure expects at runtime using .arity():

tinsel
let f = (a, b, c) -> a + b + c
assert(f.arity() == 3)

4. Lexical Closures & Upvalues

A closure captures variables from its enclosing lexical scope—even after the outer function has returned:

tinsel
fn make_counter(start = 0, step = 1) {
    let mut current = start
    () -> {
        current += step
        current
    }
}

let next_id = make_counter(100, 5)
assert(next_id() == 105)
assert(next_id() == 110)
assert(next_id() == 115)

Recursive Local Closures

To write a recursive closure assigned to a local variable (for example, when wrapping a recursive function with memoize), declare the variable with let mut first so its binding is in scope inside the closure body:

tinsel
let mut factorial = nil
factorial = n -> if n <= 1 { 1 } else { n * factorial(n - 1) }

assert(factorial(6) == 720)

5. The Forward Pipeline Operator (|>)

Nested calls like sum(filter(parse_ints(text), x -> x > 0)) force you to read code from the inside out. The pipeline operator |> rewrites transformations into a left-to-right data flow:

  • x |> f evaluates f(x).
  • x |> f(a, b) inserts x as the first argument: f(x, a, b).
  • x |> f(a, _, b) substitutes x into the _ placeholder position: f(a, x, b).
tinsel
from "std/array" import filter, map as arr_map

let raw = "readings: 12, -4, 30, -9, 18"

let total = raw
    |> parse_ints
    |> filter(x -> x > 0)
    |> arr_map(x -> x * 2)
    |> sum

assert(total == 120)

// Using the `_` placeholder to pipe into a specific argument slot:
fn divide(num, den) { num / den }
let res = 5 |> divide(100, _) // divide(100, 5)
assert(res == 20)

6. Common Pitfalls & Gotchas

Warning

Gotcha 1: Single Tuple Argument vs. Two Arguments in Arrow Lambdas (x, y) -> x + y defines a 2-argument lambda. If you are passing a callback to .map() over an array of 2-element tuples [(1, 2), (3, 4)], each element is a single tuple, so wrap the tuple pattern in outer parentheses: ((x, y)) -> x + y or [x, y] -> x + y.

Warning

Gotcha 2: Lambda Precedence (-> vs. |>) -> has lower precedence (binding power 10) than |> (binding power 20). When passing a lambda inside a pipeline call (arr |> filter(x -> x > 0)), the parentheses of filter(...) naturally group the lambda. If writing a standalone lambda that uses |>, x -> x |> f |> g means x -> ((x |> f) |> g).


7. Hands-On Exercises

Exercise 3.1: Pipeline Word Filter (Warmup)

Use .words(), filter (from std/array), and len in a |> pipeline to count how many words in "the quick brown fox jumps over the lazy dog" have at least 4 characters.

Solution & Explanation
tinsel
from "std/array" import filter

let sentence = "the quick brown fox jumps over the lazy dog"
let count = sentence.words()
    |> filter(w -> w.len() >= 4)
    |> len

assert(count == 5)
println("Words with >= 4 chars:", count)

Exercise 3.2: Function Composition Combinator (Practice)

Write a higher-order function compose_all(fns) that takes an array of 1-argument functions [f1, f2, f3] and returns a single closure that pipes an input x through each function in order.

Solution & Explanation
tinsel
fn compose_all(fns) {
    x -> fns.fold(x, (acc, f) -> f(acc))
}

let transform = compose_all([
    x -> x + 3,
    x -> x * 4,
    x -> x - 2
])

assert(transform(5) == 30) // ((5 + 3) * 4) - 2 = 30
println("Transformed:", transform(5))

Exercise 3.3: Custom Memoization Wrapper (Challenge)

Write a 1-argument memo1(fn) helper using a captured {} map closure, and use it to compute the 35th Fibonacci number in linear time.

Solution & Explanation
tinsel
fn memo1(f) {
    let cache = {}
    x -> {
        if x in cache {
            return cache[x]
        }
        let val = f(x)
        cache[x] = val
        val
    }
}

let mut fib = nil
fib = memo1(n -> if n <= 1 { n } else { fib(n - 1) + fib(n - 2) })

assert(fib(35) == 9227465)
println("Fib(35):", fib(35))

(Tip: Tinsel's standard library provides a multi-argument memoize in std/memo!)


Next Steps

In Lesson 04: Custom Structs, Options & Pattern Matching, you will learn how to model domain types with struct, handle optional values with Some/None (? and !), and dispatch cleanly with match expressions.