Functional programming is not a paradigm. It is a toolkit. The ability to pass functions as arguments, return them from other functions, and compose them into pipelines is not an academic exercise -- it is the most practical way to transform data in a web application.
Session 177 brought higher-order functions to FLIN. Not as a library import. Not as a bolted-on functional extension. As built-in methods on every list, with concise lambda syntax and full type inference. The result: data transformation pipelines that read like English, execute like native code, and catch type errors at compile time.
What Higher-Order Functions Are (and Why They Matter)
A higher-order function is a function that takes another function as an argument, or returns a function as a result. That sounds abstract. In practice, it means you can write:
active_users = users.where(u => u.is_active)instead of:
active_users = []
{for user in users}
{if user.is_active}
active_users.push(user)
{/if}
{/for}The first version says what you want (active users). The second version says how to get it (loop, check, push). In a web application where you transform data constantly -- filtering lists, mapping values, aggregating results -- the declarative version wins on readability, conciseness, and correctness. There is no off-by-one error in where. There is no forgotten push at the end of the loop. The intent is the implementation.
The Core Three: Map, Where, Reduce
map: Transform Every Element
numbers = [1, 2, 3, 4, 5]
doubled = numbers.map(n => n * 2)
// [2, 4, 6, 8, 10]users = User.all names = users.map(u => u.name) // ["Juste", "Thales", "Claude"]
// With block body for complex transforms summaries = orders.map(order => { total = order.items.sum(i => i.price) "{order.id}: {total.format(2)} XOF" }) ```
map applies a function to every element of a list and returns a new list of the results. The original list is never modified. The type checker infers the result type from the lambda's return type: if you map [int] with n => n * 2, the result is [int]. If you map [User] with u => u.name, the result is [text].
where: Keep Matching Elements
FLIN uses where instead of filter for a deliberate reason: it reads more naturally in English. "Users where active" is a complete thought. "Users filter active" is not.
numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
evens = numbers.where(n => n.is_even)
// [2, 4, 6, 8, 10]adults = users.where(u => u.age >= 18) active_admins = users.where(u => u.is_active and u.role == "admin") ```
where returns a new list containing only the elements for which the predicate returns true. The type of the result list matches the type of the input list: filtering [User] produces [User].
The complement of where is reject:
inactive = users.reject(u => u.is_active)
// Same as: users.where(u => not u.is_active)reduce: Collapse a List to a Single Value
reduce is the most powerful higher-order function. Every other list operation -- sum, map, where, count, any, all -- can be expressed as a reduce. It takes an initial accumulator value and a function that combines the accumulator with each element:
numbers = [1, 2, 3, 4, 5]
total = numbers.reduce(0, (acc, n) => acc + n)
// 15// Build a string names = ["Juste", "Thales", "Claude"] sentence = names.reduce("", (acc, name) => { {if acc.is_empty} name {else} "{acc}, {name}" {/if} }) // "Juste, Thales, Claude"
// Build a map from a list frequency = words.reduce({}, (acc, word) => { count = acc.get(word, 0) acc.set(word, count + 1) }) // { "hello": 3, "world": 2, ... } ```
The type of the accumulator and the return type of the lambda must match. If you start with 0 (an int) and the lambda returns acc + n (also an int), the result is int. If you start with "" (a text) and the lambda returns a text, the result is text. The type checker enforces this automatically.
Beyond the Core Three
flat_map: Map and Flatten
// Each user has a list of orders
users_orders = users.flat_map(u => u.orders)
// Flat list of all orders from all users// Generate multiple items per input numbers = [1, 2, 3] expanded = numbers.flat_map(n => [n, n * 10]) // [1, 10, 2, 20, 3, 30] ```
flat_map is map followed by flatten. It applies a function that returns a list for each element, then concatenates all the result lists into a single flat list. This is essential for one-to-many transformations.
zip and zip_with: Combine Two Lists
names = ["Juste", "Thales", "Claude"]
ages = [28, 28, 0]pairs = names.zip(ages) // [["Juste", 28], ["Thales", 28], ["Claude", 0]]
sentences = names.zip_with(ages, (name, age) => "{name} is {age}") // ["Juste is 28", "Thales is 28", "Claude is 0"] ```
zip combines two lists element by element into a list of pairs. zip_with does the same but applies a function to each pair. If the lists have different lengths, zip stops at the shorter list.
partition: Split Into Two Groups
numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
result = numbers.partition(n => n.is_even)
evens = result[0] // [2, 4, 6, 8, 10]
odds = result[1] // [1, 3, 5, 7, 9]// Separate valid and invalid entries result = entries.partition(e => e.is_valid) valid = result[0] invalid = result[1] ```
partition is where and reject in a single pass. Instead of iterating the list twice (once to find matches, once to find non-matches), it iterates once and separates elements into two lists.
group_by: Categorize Elements
users = User.all
by_role = users.group_by(u => u.role)
// {
// "admin": [User{...}, User{...}],
// "user": [User{...}, User{...}, User{...}],
// "moderator": [User{...}]
// }// Group orders by month by_month = orders.group_by(o => o.created_at.format("YYYY-MM")) // { "2026-01": [...], "2026-02": [...], "2026-03": [...] } ```
group_by is one of the most useful higher-order functions for data display. Grouping users by role, orders by date, products by category -- these are patterns that appear in every dashboard and admin panel.
chunk: Split Into Fixed-Size Groups
items = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
pages = items.chunk(3)
// [[1, 2, 3], [4, 5, 6], [7, 8, 9], [10]]chunk splits a list into sublists of a specified size. The last chunk may be smaller. This is useful for pagination, grid layout (displaying items in rows), and batch processing.
Testing Predicates: any, all, none
numbers = [2, 4, 6, 8, 10]numbers.any(n => n > 5) // true (at least one is > 5) numbers.all(n => n.is_even) // true (all are even) numbers.none(n => n < 0) // true (none are negative) ```
These three functions test whether a predicate holds for any, all, or no elements of a list. They short-circuit: any stops as soon as it finds a match, all stops as soon as it finds a non-match, none stops as soon as it finds a match. For large lists, short-circuiting is a significant performance advantage.
Lambda Syntax
FLIN's lambda syntax is designed for conciseness without sacrificing clarity:
// Single parameter, expression body
list.map(x => x * 2)// Single parameter, inferred types list.where(user => user.is_active)
// Multiple parameters list.reduce(0, (acc, x) => acc + x)
// Block body for multi-line logic list.map(item => { processed = item.name.trim.lower score = item.values.sum { name: processed, score: score } }) ```
Lambdas capture variables from their enclosing scope:
threshold = 100
expensive = orders.where(o => o.total > threshold)
// 'threshold' is captured by the lambdaThe compiler detects which variables a lambda captures and stores them in a closure object on the heap. The closure is created once when the lambda expression is evaluated and called multiple times by the higher-order function. Captured variables are shared references -- modifying a captured variable inside the lambda modifies the original.
Method Chaining: The Pipeline Pattern
The real power of higher-order functions emerges when you chain them:
// Find the top 5 most expensive items purchased by active users this month
result = orders
.where(o => o.user.is_active)
.where(o => o.created_at.is_after(now.start_of_month))
.flat_map(o => o.items)
.sort_by(item => item.price)
.reverse
.take(5)
.map(item => {
name: item.name,
price: item.price.format(2)
})This chain reads top to bottom as a data flow: 1. Start with all orders 2. Keep only orders from active users 3. Keep only orders from this month 4. Extract all items from those orders (flatten) 5. Sort by price 6. Reverse (highest first) 7. Take the top 5 8. Format each item for display
Each step produces a new list. No step modifies the original data. The entire pipeline is immutable, predictable, and easy to debug -- insert a debug() call at any point to see the intermediate result.
Type Inference in Chains
One of the most impressive aspects of FLIN's higher-order function implementation is how the type checker handles chains. Each step's output type becomes the next step's input type, inferred automatically:
// The type checker tracks types through the entire chain:
result = users // [User]
.where(u => u.age > 18) // [User] (where preserves type)
.map(u => u.email) // [text] (map transforms type)
.where(e => e.contains("@flin")) // [text] (where preserves type)
.sort // [text] (sort preserves type)If you write .map(u => u.nonexistent_field) at step 2, the type checker catches it immediately -- User has no field called nonexistent_field. If you write .where(e => e * 2) at step 3, the type checker catches it -- the predicate must return bool, not int. Errors are caught at the step where they occur, not propagated to the end of the chain.
Implementation: Closures and the Value Stack
Higher-order functions are implemented as built-in methods that accept closures. The closure is a value on the stack, just like a number or a string. When map executes, it:
1. Pops the closure and the list from the stack 2. Allocates a new result list 3. For each element in the input list: a. Pushes the element onto the stack b. Calls the closure c. Pops the result and appends it to the result list 4. Pushes the result list onto the stack
fn exec_list_map(&mut self) -> Result<(), VmError> {
let closure = self.pop_closure()?;
let list = self.pop_list()?;
let mut result = Vec::with_capacity(list.len());for element in list.iter() { self.push(element.clone()); self.call_closure(&closure, 1)?; result.push(self.pop()?); }
let result_id = self.heap.alloc_list(result); self.push(Value::List(result_id)); Ok(()) } ```
The call_closure function sets up a new call frame, pushes the closure's captured variables, executes the closure's bytecode, and returns control to the caller. The overhead per element is approximately 5-10 nanoseconds -- fast enough that mapping over a list of 10,000 elements takes 50-100 microseconds.
For reduce, the implementation is similar but carries the accumulator:
fn exec_list_reduce(&mut self) -> Result<(), VmError> {
let closure = self.pop_closure()?;
let list = self.pop_list()?;
let mut acc = self.pop()?; // Initial accumulatorfor element in list.iter() { self.push(acc); self.push(element.clone()); self.call_closure(&closure, 2)?; acc = self.pop()?; }
self.push(acc); Ok(()) } ```
The Twelve Higher-Order Functions
The complete set of higher-order list functions:
| Function | Signature | Purpose |
|---|---|---|
map | (T -> U) -> [U] | Transform each element |
where | (T -> bool) -> [T] | Keep matching elements |
reject | (T -> bool) -> [T] | Remove matching elements |
reduce | (U, (U,T) -> U) -> U | Collapse to single value |
flat_map | (T -> [U]) -> [U] | Map and flatten |
find | (T -> bool) -> T? | First match or none |
find_index | (T -> bool) -> int? | Index of first match |
any | (T -> bool) -> bool | True if any match |
all | (T -> bool) -> bool | True if all match |
none_match | (T -> bool) -> bool | True if none match |
partition | (T -> bool) -> [[T],[T]] | Split into two groups |
group_by | (T -> K) -> map[K,[T]] | Group by key |
Twelve functions that replace imperative loops in 90% of data transformation code. Each one is a built-in method on every list. No imports. No functional programming library. Just .map, .where, and .reduce -- the tools that every web developer reaches for when transforming data for display.
---
This is Part 78 of the "How We Built FLIN" series, documenting how a CEO in Abidjan and an AI CTO built higher-order functions into a programming language with full type inference.
Series Navigation: - [77] Introspection and Reflection at Runtime - [78] Reduce, Map, Filter: Higher-Order Functions (you are here) - [79] Validation and Sanitization Functions - [80] Error Tracking and Performance Monitoring