Chapter 5: Functions #
"Functions are the verbs of programming. Variables are the nouns. A well-written program reads like clear prose."
Up to this point, every program you have written has been a flat sequence of statements inside the main block. That works for small examples, but real programs need structure. Functions let you name a block of code, give it parameters, and call it from anywhere -- as many times as you need.
In AI agent systems, every skill an agent possesses is a function. When an agent decides to search a database, call an API, or format a response, it is invoking a function you wrote. Master functions, and you master the building blocks of intelligent agent behavior. Whether you are a student writing your first program, a professional building production systems, or an AI operations engineer designing agent pipelines -- functions are where the real work happens.
By the end of this chapter, you will be able to:
- Define functions with
fun - Pass arguments and return values
- Write recursive functions
- Control function visibility with
pub,crate, andsuper - Understand higher-order patterns
- Return multiple values using tuples and destructure the results
- Use destructuring in function parameters
- Build a library of reusable utility functions
- Use the pipe operator for readable function composition
- Import and use standard library modules
Functions Are Recipes
Think of a function like a recipe in a cookbook. A recipe has a name ("Chocolate Cake"), a list of ingredients (parameters), a set of steps (the body), and a final result (the return value). Once you write the recipe, anyone can follow it -- and they can follow it as many times as they want without rewriting the instructions. Just as a professional kitchen organizes work into recipes that different chefs can execute independently, a well-structured program organizes work into functions that can be called from anywhere.
5.1 Defining Functions with fun #
You need to greet 100 different users by name. Do you
write 100 separate emit statements? Or do you write the greeting logic once
and reuse it?
A function in Neam is declared with the fun keyword, followed by a name, a
parameter list in parentheses, and a body in curly braces:
fun greet(name) {
return "Hello, " + name + "!";
}
{
emit greet("World");
emit greet("Neam");
emit greet("Alice");
}
Expected output:
Hello, World!
Hello, Neam!
Hello, Alice!
F-strings (introduced in Chapter 4) often make function output more readable.
The greet function above could also be written as:
return f"Hello, {name}!"; instead of return "Hello, " + name + "!";
We will show both styles throughout this chapter so you are comfortable with each.
The general form is:
fun <name>(<param1>, <param2>, ...) {
// body
return <value>;
}
Key rules:
- Functions are defined outside the main execution block
{ ... }. - The main block calls functions; functions do not contain the main block.
- Function names follow the same rules as variable names: start with a letter or underscore, then letters, digits, or underscores.
- By convention, function names use
snake_case:calculate_total,is_valid,format_output.
Where Functions Live #
This is the standard layout of a Neam program:
// 1. Function definitions (outside the main block)
fun add(a, b) {
return a + b;
}
fun multiply(a, b) {
return a * b;
}
// 2. Main execution block (entry point)
{
let sum = add(3, 4);
let product = multiply(5, 6);
emit f"Sum: {sum}";
emit f"Product: {product}";
}
Functions must be defined before (or at the same level as) the main block. The compiler processes function definitions first, so you can call a function that is defined later in the file -- but the conventional style is to define functions above the main block.
5.2 Parameters and Arguments #
Parameters are the names listed in a function's definition. Arguments are the values you pass when you call the function.
fun format_price(amount, currency) {
return currency + str(amount);
}
{
emit format_price(29.99, "$"); // $29.99
emit format_price(1500, "EUR "); // EUR 1500
}
How Arguments Are Passed #
Neam passes arguments by value. This means the function receives a copy of each value. Modifying a parameter inside the function does not affect the original variable:
fun try_to_change(x) {
x = 999;
return x;
}
{
let original = 42;
let result = try_to_change(original);
emit "Original: " + str(original); // 42 -- unchanged
emit "Result: " + str(result); // 999
}
Functions with No Parameters #
A function that takes no arguments still needs empty parentheses:
fun get_greeting() {
return "Hello, World!";
}
{
emit get_greeting();
}
Functions with Multiple Parameters #
There is no fixed limit on the number of parameters:
fun create_full_name(first, middle, last) {
return f"{first} {middle} {last}";
}
{
emit create_full_name("Ada", "Augusta", "Lovelace");
}
Write a function format_price(amount, currency) that uses an f-string to
return a formatted price. For example, format_price(29.99, "USD") should
return "USD 29.99". Call it with at least three different currencies.
5.3 Return Values #
Your function calculates a value -- but how does the caller get that value back? And what happens if you forget to return it?
Functions return values with the return keyword:
fun square(n) {
return n * n;
}
{
emit str(square(5)); // 25
emit str(square(12)); // 144
}
Implicit Return of nil #
If a function reaches the end of its body without hitting a return statement,
it implicitly returns nil:
fun say_hello(name) {
emit "Hello, " + name + "!";
// No return statement -- returns nil
}
{
let result = say_hello("Bob");
emit "Returned: " + str(result);
}
Expected output:
Hello, Bob!
Returned: nil
Forgetting return
One of the most frequent bugs for beginners is forgetting the return keyword.
If your function computes a value but does not return it, the caller gets nil
instead. This can be especially confusing because the function appears to work
-- it just silently gives back the wrong value.
// BUG: forgot to return!
fun add_broken(a, b) {
let result = a + b;
// Missing: return result;
}
// FIX: always return the computed value
fun add_fixed(a, b) {
let result = a + b;
return result;
}
If you are getting unexpected nil values, the first thing to check is whether
your function has a return statement on every code path.
Early Return #
You can use return to exit a function early:
fun absolute_value(n) {
if (n < 0) {
return -n;
}
return n;
}
{
emit str(absolute_value(-7)); // 7
emit str(absolute_value(3)); // 3
emit str(absolute_value(0)); // 0
}
Returning Different Types #
Because Neam is dynamically typed, a function can return different types depending on the input. While this is possible, it is generally better practice to have functions return a consistent type:
// This works but can be confusing
fun flexible(x) {
if (typeof(x) == "number") {
return x * 2;
}
if (typeof(x) == "string") {
return x + x;
}
return nil;
}
{
emit str(flexible(5)); // 10
emit flexible("ha"); // haha
emit str(flexible(true)); // nil
}
Returning Multiple Values with Tuples #
Sometimes a function needs to return more than one value. In many languages, you would need to return a map or a list. In Neam, you can return a tuple and let the caller destructure the result:
fun divide(a, b) {
return (a / b, a % b);
}
{
let (quotient, remainder) = divide(10, 3);
emit f"10 / 3 = {quotient} remainder {remainder}";
// 10 / 3 = 3.3333333333333335 remainder 1
}
This pattern is called multi-return. The function returns a single tuple value, and the caller uses tuple destructuring (introduced in Chapter 4) to unpack it into separate variables.
Here is a more practical example -- a function that returns both the minimum and maximum of a list in a single pass:
fun min_max(items) {
let lo = items[0];
let hi = items[0];
for item in items {
if (item < lo) { lo = item; }
if (item > hi) { hi = item; }
}
return (lo, hi);
}
{
let (low, high) = min_max([8, 3, 11, 1, 7, 15, 2]);
emit f"Range: {low} to {high}"; // Range: 1 to 15
}
+-----------------------------------------------------------+
| Multi-Return Flow |
| |
| min_max([8, 3, 11, 1, 7, 15, 2]) |
| | |
| v |
| returns (1, 15) <-- a single tuple value |
| | |
| v |
| let (low, high) = ... <-- destructured into two vars |
| | | |
| v v |
| low = 1 high = 15 |
+-----------------------------------------------------------+
Multi-return is especially useful in agent development. A skill function might return both a result and a confidence score, or a parser might return both the parsed value and the remaining unparsed input. You will see this pattern used throughout the later chapters.
5.4 The Function Call Stack #
When a function calls another function (or itself), the VM maintains a call stack to keep track of where to return. Understanding this is essential for understanding recursion and debugging.
Each function call creates a stack frame that holds:
- The function's local variables and parameters
- The return address (where to continue after the function finishes)
When a function returns, its stack frame is removed and execution continues at the call site.
5.5 Recursive Functions #
A recursive function is one that calls itself. Recursion is a powerful technique for problems that can be broken down into smaller versions of the same problem.
The Classic: Factorial #
The factorial of n (written n!) is the product of all integers from 1 to n:
5! = 5 * 4 * 3 * 2 * 1 = 1201! = 10! = 1(by definition)
In Neam:
fun factorial(n) {
if (n <= 1) {
return 1;
}
return n * factorial(n - 1);
}
{
emit f"5! = {factorial(5)}"; // 120
emit f"10! = {factorial(10)}"; // 3628800
emit f"0! = {factorial(0)}"; // 1
}
How Recursion Works #
Let us trace factorial(4):
factorial(4)
-> 4 * factorial(3)
-> 3 * factorial(2)
-> 2 * factorial(1)
-> returns 1 (base case)
-> returns 2 * 1 = 2
-> returns 3 * 2 = 6
-> returns 4 * 6 = 24
Every recursive function needs two things:
- A base case that stops the recursion. Without it, the function would call itself forever and eventually crash with a stack overflow.
- A recursive case that makes progress toward the base case. Each recursive call must bring you closer to the stopping condition.
Fibonacci #
The Fibonacci sequence is another classic recursive example. Each number is the sum of the two preceding ones: 0, 1, 1, 2, 3, 5, 8, 13, 21, ...
fun fibonacci(n) {
if (n <= 1) {
return n;
}
return fibonacci(n - 1) + fibonacci(n - 2);
}
{
// Print the first 10 Fibonacci numbers (using while loop)
let i = 0;
while (i < 10) {
emit f"fib({i}) = {fibonacci(i)}";
i = i + 1;
}
}
You can also use a for loop with range() for a cleaner version:
fun fibonacci(n) {
if (n <= 1) {
return n;
}
return fibonacci(n - 1) + fibonacci(n - 2);
}
{
// Using for-loop (cleaner)
for (i in range(10)) {
emit f"fib({i}) = {fibonacci(i)}";
}
}
Expected output (same for both versions):
fib(0) = 0
fib(1) = 1
fib(2) = 1
fib(3) = 2
fib(4) = 3
fib(5) = 5
fib(6) = 8
fib(7) = 13
fib(8) = 21
fib(9) = 34
Performance note: This naive recursive Fibonacci has exponential time
complexity. For large values of n, it becomes extremely slow because it
recomputes the same values many times. In Chapter 6, you will see how to
write an iterative version using a while loop that runs in linear time.
The Fibonacci function above recomputes the same values repeatedly. Can you
add emit statements inside the function to see how many times fibonacci(5)
calls itself? (Hint: add emit f"computing fib({n})" as the first line of
the function body and count the output lines.)
5.6 Destructuring in Function Parameters and Return Values #
In Chapter 4, you learned about destructuring -- unpacking collections into individual variables. This pattern becomes even more powerful when combined with functions. You can destructure tuples, lists, and maps directly at the call site when receiving a function's return value.
Tuple Destructuring from Function Returns #
The most common pattern is receiving a multi-return tuple:
fun get_point() {
return (3.5, 7.2);
}
{
let (x, y) = get_point();
emit f"Point: ({x}, {y})"; // Point: (3.5, 7.2)
}
List Destructuring with Rest #
When a function returns a list, you can destructure it with the spread operator to capture the head and tail:
fun get_items() {
return ["urgent", "normal", "low", "trivial"];
}
{
let [first, ...rest] = get_items();
emit f"Priority: {first}"; // Priority: urgent
emit f"Others: {rest}"; // Others: ["normal", "low", "trivial"]
}
Map Destructuring #
When a function returns a map, you can destructure it into named variables that match the keys:
fun get_person() {
return {"name": "Alice", "age": 30, "city": "London"};
}
{
let {name, age} = get_person();
emit f"{name} is {age} years old"; // Alice is 30 years old
}
The variable names must match the map's keys. Only the keys you list are extracted -- other keys are ignored.
Combining Patterns #
These patterns compose naturally. Here is a function that returns structured data, consumed by destructuring:
fun analyze_scores(scores) {
let total = fold(scores, 0, fn(acc, x) { return acc + x; });
let count = len(scores);
let average = total / count;
let sorted = sort(scores);
let [lowest, ...middle] = sorted;
return {
"average": average,
"lowest": lowest,
"highest": sorted[len(sorted) - 1]
};
}
{
let {average, lowest, highest} = analyze_scores([82, 95, 67, 91, 73]);
emit f"Average: {average}"; // Average: 81.6
emit f"Range: {lowest}-{highest}"; // Range: 67-95
}
Destructuring is not limited to function return values. You can use it with any expression that produces a tuple, list, or map. But it pairs especially well with functions because it eliminates the need for temporary variables and indexing.
5.7 Function Visibility: pub, crate, super #
When you organize Neam code across multiple files using the module system, you need to control which functions are accessible from outside the current module. Neam provides three visibility modifiers:
| Modifier | Meaning |
|---|---|
| (none) | Private -- accessible only within the same file |
pub |
Public -- accessible from any module that imports this one |
crate |
Crate-visible -- accessible within the same project/package |
super |
Parent-visible -- accessible from the parent module |
Public Functions #
// file: utils.neam
module myproject::utils;
/// Doubles a number. Accessible from any importing module.
pub fun double(x) {
return x * 2;
}
/// Internal helper. Not accessible outside this file.
fun internal_helper(x) {
return x + 1;
}
// file: main.neam
import myproject::utils;
{
emit str(utils::double(21)); // 42
// utils::internal_helper(5); // ERROR: not accessible
}
Crate Visibility #
crate makes a function visible to any file within the same package but not
to external packages that depend on yours:
module myproject::internal;
crate fun shared_secret() {
return "only within this project";
}
Super Visibility #
super makes a function visible to the parent module:
module myproject::math::helpers;
super fun normalize(x, max) {
return x / max;
}
The normalize function is accessible from myproject::math but not from
myproject or any other module.
Choosing the Right Visibility #
For single-file programs (which is what you are writing in these early chapters), visibility modifiers are not needed. All functions are accessible within the file. As your programs grow into multi-file projects, follow this guideline:
- Start with no modifier (private).
- Add
pubonly for functions that form the public API of your module. - Use
cratefor shared internal utilities. - Use
supersparingly for tightly coupled parent-child module relationships.
5.8 Anonymous Functions and Lambdas with fn #
Neam supports anonymous functions -- functions without a name -- using the fn
keyword. These are sometimes called lambdas or closures in other languages.
Use fun to declare named functions (fun greet(name) { ... }).
Use fn to create anonymous functions (fn(x) { x * 2 }). This distinction
is consistent throughout Neam.
{
// An anonymous function that doubles a number
let doubler = fn(x) { return x * 2; };
emit str(doubler(5)); // 10
emit str(doubler(21)); // 42
}
The general form is:
fn(<params>) { <body> }
Anonymous functions are values. You can assign them to variables, pass them as arguments, and return them from other functions.
Concise Lambda Syntax #
When an anonymous function body is a single expression, you can omit the
return keyword. The last expression in the body is automatically returned:
{
// Verbose form (with explicit return)
let doubler_v1 = fn(x) { return x * 2; };
// Concise form (implicit return of the last expression)
let doubler_v2 = fn(x) { x * 2 };
emit str(doubler_v1(5)); // 10
emit str(doubler_v2(5)); // 10 -- same result
}
This concise style is especially handy when passing short lambdas to higher-order functions:
{
let numbers = [1, 2, 3, 4, 5];
// Concise lambdas -- no 'return', no semicolon inside the body
let doubled = map(numbers, fn(x) { x * 2 });
let evens = filter(numbers, fn(x) { x % 2 == 0 });
emit str(doubled); // [2, 4, 6, 8, 10]
emit str(evens); // [2, 4]
}
Both styles -- verbose with return and concise without -- are valid Neam.
Use whichever is clearer for the situation. A good rule of thumb: if the body
fits on one line, use the concise form; if it needs multiple statements, use
explicit return.
Why Anonymous Functions Matter #
Anonymous functions are essential for working with Neam's built-in higher-order
functions like map, filter, and fold:
{
let numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
// Filter: keep only even numbers
let evens = filter(numbers, fn(x) { return x % 2 == 0; });
emit str(evens); // [2, 4, 6, 8, 10]
// Map: double each number
let doubled = map(numbers, fn(x) { return x * 2; });
emit str(doubled); // [2, 4, 6, 8, 10, 12, 14, 16, 18, 20]
// Fold: sum all numbers (starting from 0)
let total = fold(numbers, 0, fn(acc, x) { return acc + x; });
emit str(total); // 55
}
Without anonymous functions, you would need to define a separate named function for each operation -- even if you only use it once. Anonymous functions keep the logic inline where it is used.
Closures: Capturing Outer Variables #
Anonymous functions can "capture" variables from their surrounding scope. This is called a closure:
{
let threshold = 5;
// This fn captures 'threshold' from the outer scope
let above_threshold = filter(
[1, 3, 5, 7, 9],
fn(x) { return x > threshold; }
);
emit str(above_threshold); // [7, 9]
}
The anonymous function fn(x) { return x > threshold; } uses threshold, which
is defined outside the function. The anonymous function "closes over" this variable,
hence the name closure.
Passing Named Functions #
You can also pass a named function wherever an anonymous function is expected:
fun is_positive(x) {
return x > 0;
}
{
let numbers = [-3, -1, 0, 2, 5];
let positives = filter(numbers, is_positive);
emit str(positives); // [2, 5]
}
5.9 Higher-Order Functions #
A higher-order function is a function that takes another function as a parameter or returns a function. Neam's standard library includes several built-in higher-order functions that you will use constantly.
map -- Transform Every Element #
map(list, fn) applies a function to each element and returns a new list:
{
let names = ["alice", "bob", "charlie"];
// Capitalize each name
let capitalized = map(names, fn(name) { return name.upper(); });
emit str(capitalized); // ["ALICE", "BOB", "CHARLIE"]
// Compute squares
let squares = map([1, 2, 3, 4, 5], fn(x) { return x * x; });
emit str(squares); // [1, 4, 9, 16, 25]
}
filter -- Select Matching Elements #
filter(list, fn) keeps only elements for which the function returns true:
{
let scores = [45, 78, 92, 33, 88, 67, 95];
let passing = filter(scores, fn(s) { return s >= 60; });
emit "Passing: " + str(passing); // [78, 92, 88, 67, 95]
let honors = filter(scores, fn(s) { return s >= 90; });
emit "Honors: " + str(honors); // [92, 95]
}
fold -- Accumulate a Result #
fold(list, initial, fn) reduces a list to a single value by applying a function
that takes an accumulator and the current element:
{
let numbers = [1, 2, 3, 4, 5];
// Sum: start at 0, add each number
let sum = fold(numbers, 0, fn(acc, x) { return acc + x; });
emit f"Sum: {sum}"; // Sum: 15
// Product: start at 1, multiply each number
let product = fold(numbers, 1, fn(acc, x) { return acc * x; });
emit f"Product: {product}"; // Product: 120
// Build a string
let words = ["Neam", "is", "powerful"];
let sentence = fold(words, "", fn(acc, w) {
if (len(acc) == 0) { return w; }
return acc + " " + w;
});
emit sentence; // Neam is powerful
}
find -- Locate the First Match #
find(list, fn) returns the first element for which the function returns true,
or nil if no element matches:
{
let users = [
{"name": "Alice", "role": "admin"},
{"name": "Bob", "role": "user"},
{"name": "Carol", "role": "admin"}
];
let first_admin = find(users, fn(u) { return u["role"] == "admin"; });
emit "First admin: " + first_admin["name"]; // First admin: Alice
}
sort_by -- Custom Sort Order #
sort_by(list, fn) sorts elements using a function that extracts the
comparison key from each element:
{
let people = [
{"name": "Carol", "age": 28},
{"name": "Alice", "age": 35},
{"name": "Bob", "age": 22}
];
// Sort by age
let by_age = sort_by(people, fn(p) { p["age"] });
for person in by_age {
emit f"{person["name"]}: {person["age"]}";
}
// Bob: 22
// Carol: 28
// Alice: 35
// Sort by name
let by_name = sort_by(people, fn(p) { p["name"] });
for person in by_name {
emit person["name"];
}
// Alice
// Bob
// Carol
}
group_by -- Partition into Groups #
group_by(list, fn) partitions elements into a map of lists, where the keys
are the return values of the grouping function:
{
let words = ["apple", "ant", "banana", "avocado", "blueberry", "cherry"];
// Group by first letter
let grouped = group_by(words, fn(w) { w.substring(0, 1) });
emit str(grouped);
// {"a": ["apple", "ant", "avocado"], "b": ["banana", "blueberry"], "c": ["cherry"]}
// Group numbers by even/odd
let numbers = [1, 2, 3, 4, 5, 6, 7, 8];
let parity = group_by(numbers, fn(n) {
if (n % 2 == 0) { return "even"; }
return "odd";
});
emit f"Even: {parity["even"]}"; // Even: [2, 4, 6, 8]
emit f"Odd: {parity["odd"]}"; // Odd: [1, 3, 5, 7]
}
Higher-Order Functions Reference #
Here is a summary of all the higher-order collection functions:
| Function | Signature | Description |
|---|---|---|
map |
map(list, fn) |
Transform each element |
filter |
filter(list, fn) |
Keep elements where fn returns true |
fold |
fold(list, init, fn) |
Reduce to a single value |
find |
find(list, fn) |
First element where fn returns true |
sort_by |
sort_by(list, fn) |
Sort by a computed key |
group_by |
group_by(list, fn) |
Partition into groups by key |
Chaining Higher-Order Functions #
The real power comes from chaining these operations together:
{
let data = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
// Find the sum of squares of even numbers
let result = fold(
map(
filter(data, fn(x) { return x % 2 == 0; }),
fn(x) { return x * x; }
),
0,
fn(acc, x) { return acc + x; }
);
emit "Sum of squares of evens: " + str(result); // 220
// (4 + 16 + 36 + 64 + 100 = 220)
}
Function Composition #
You can chain function calls, passing the result of one as the argument to another:
fun clean_text(text) {
return text.lower();
}
fun add_prefix(prefix, text) {
return prefix + text;
}
fun format_tag(text) {
let cleaned = clean_text(text);
let tagged = add_prefix("#", cleaned);
return tagged;
}
{
emit format_tag("NeamLang"); // #neamlang
emit format_tag("AI Agent"); // #ai agent
}
5.9.1 The Pipe Operator for Function Composition #
In the "Chaining Higher-Order Functions" example above, the nested calls were hard to read -- you had to read from the inside out:
// Nested calls -- read inside-out (hard to follow)
let result = fold(map(filter(data, fn(x) { return x % 2 == 0; }), fn(x) { return x * x; }), 0, fn(acc, x) { return acc + x; });
The pipe operator (|>) lets you rewrite this as a left-to-right pipeline.
The pipe operator takes the value on the left and passes it as the first
argument to the function on the right. It is left-associative and desugars at
compile time, so there is no runtime overhead:
{
let data = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
// Use the pipe operator -- read top-to-bottom, left-to-right
let result = data
|> filter(fn(x) { return x % 2 == 0; })
|> map(fn(x) { return x * x; })
|> fold(0, fn(acc, x) { return acc + x; });
emit f"Sum of squares of evens: {result}"; // 220
}
Each line in the pipeline describes one transformation step:
┌──────────────────────────────────────────────────────────┐
│ Pipe Operator with Higher-Order Functions │
│ │
│ data ──→ filter(even?) ──→ map(square) ──→ fold(sum) │
│ │
│ [1, 2, 3, 4, 5, 6, 7, 8, 9, 10] │
│ │ │
│ ▼ │
│ [2, 4, 6, 8, 10] (keep even numbers) │
│ │ │
│ ▼ │
│ [4, 16, 36, 64, 100] (square each) │
│ │ │
│ ▼ │
│ 220 (sum all) │
│ │
└──────────────────────────────────────────────────────────┘
The pipe operator also works with method calls using the dot-prefix syntax. This is especially useful for list operations:
{
let scores = [88, 95, 72, 91, 85, 78, 99, 63];
// Chain method calls with pipe -- each step is clear
let top3 = scores |> .sort() |> .reverse() |> .take(3);
emit f"Top 3 scores: {top3}"; // Top 3 scores: [99, 95, 91]
}
How Pipe Desugars #
The pipe operator is purely syntactic sugar. The compiler rewrites it at compile time, so there is zero runtime overhead. Here is what the desugaring looks like:
// What you write:
data |> filter(fn(x) { x > 0 }) |> map(fn(x) { x * 2 }) |> take(10)
// What the compiler sees (left-associative):
take(map(filter(data, fn(x) { x > 0 }), fn(x) { x * 2 }), 10)
The left side becomes the first argument of the function call on the right.
Because the pipe is left-associative, a |> b(x) |> c(y) means
c(b(a, x), y).
Pipe with Concise Lambdas #
The pipe operator pairs especially well with the concise lambda syntax
(no return), resulting in very readable data pipelines:
{
let data = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
let result = data
|> filter(fn(x) { x > 0 })
|> map(fn(x) { x * 2 })
|> take(10);
emit str(result); // [2, 4, 6, 8, 10, 12, 14, 16, 18, 20]
}
The expr |> .method(args) syntax calls the method on the piped value.
This form is useful for chaining method calls:
{
let result = "Hello, World!"
|> .lower()
|> .words()
|> .to_set();
emit str(result); // {"hello,", "world!"}
}
When to use pipe vs. nested calls:
| Style | Best for | Example |
|---|---|---|
| Nested calls | 1-2 function deep | str(square(5)) |
| Pipe operator | 3+ steps in a chain | data \|> filter(...) \|> map(...) \|> fold(...) |
| Intermediate variables | Complex logic with branching | let filtered = filter(...); let mapped = map(filtered, ...); |
The pipe operator was introduced in Chapter 4 (Section 4.9). Here, you are seeing it combined with higher-order functions -- a pattern you will use extensively in agent data processing pipelines.
Given the list ["alice", "BOB", "Charlie", "dave"], use the pipe operator
to create a pipeline that: (1) maps each name to lowercase, (2) filters names
longer than 3 characters, and (3) sorts the result alphabetically. What do
you get?
5.10 Functions in Agent Development #
Before moving to practical examples, it is worth understanding how functions connect to AI agent programming. Functions are not just organizational tools in Neam -- they are the mechanism through which agents interact with the world.
Callbacks for Agent Events #
When agents perform handoffs or skill calls, you can attach callback functions that execute when specific events occur:
// Callback triggered when an agent hands off to tech support
fun on_tech_handoff(context) {
emit "[LOG] Technical support handoff triggered";
emit "[LOG] User query: " + context["query"];
}
// Callback triggered when a skill is executed
fun on_skill_call(skill_name, params) {
emit "[AUDIT] Skill called: " + skill_name;
}
These callbacks are passed by name to agent configurations (covered in Chapter 13).
The pattern is the same as passing a function to filter or map -- you are
giving the agent a function to call when something happens.
Functions as Skill Implementations #
In Neam, when you define a skill that an agent can call, the skill's behavior is implemented as a function. A skill wraps a function with metadata (description, parameters) so the LLM knows when and how to invoke it:
skill SearchProducts {
description: "Search the product database",
params: ["query", "max_results"],
impl: fun(query, max_results) {
// In a real program, this would call an API or database
let results = [
{"name": "Laptop", "price": 999},
{"name": "Keyboard", "price": 79}
];
return results;
}
}
The impl field contains the function that runs when the skill is invoked. When
an LLM decides to call SearchProducts, Neam invokes this function with the
parameters the LLM chose. The full skill syntax is covered in Chapter 12.
Processing Agent Responses #
Functions are essential for parsing and transforming agent responses:
fun extract_decision(response) {
if (response.contains("APPROVED")) {
return "approved";
}
if (response.contains("REJECTED")) {
return "rejected";
}
return "pending";
}
fun format_agent_report(agent_name, response, decision) {
return f"{agent_name} | {decision} | {response.substring(0, 50)}";
}
These utility functions form the glue between agent calls and application logic. The patterns you learn in this chapter -- parameters, return values, composition, higher-order functions -- are exactly the patterns you will use when building agent systems.
Standard Library Functions #
You do not need to write every function from scratch. Neam's standard library
provides a collection of ready-made functions organized into modules. You import
them with the import statement:
import std.math::{sqrt, abs, pi};
import std.crypto::{sha256, uuid_v4};
fun distance(x1, y1, x2, y2) {
return sqrt((x2 - x1) * (x2 - x1) + (y2 - y1) * (y2 - y1));
}
fun generate_record_id(data) {
return uuid_v4();
}
{
let d = distance(0, 0, 3, 4);
emit f"Distance: {d}"; // Distance: 5
let id = generate_record_id("test");
emit f"Record ID: {id}"; // Record ID: (a UUID string)
emit f"Pi = {pi}"; // Pi = 3.141592653589793
emit f"abs(-7) = {abs(-7)}"; // abs(-7) = 7
}
Some commonly used standard library modules:
| Module | Provides |
|---|---|
std.math |
sqrt, abs, pi, floor, ceil, round, pow, log |
std.crypto |
sha256, uuid_v4, hmac_sha256 |
std.time |
now, sleep, format_date |
std.io |
read_file, write_file, read_line |
The full module system and import syntax are covered in Chapter 9. For now, just know that you can pull in these building blocks whenever you need them.
5.11 Practical Examples #
Let us build several useful functions that demonstrate the concepts from this chapter.
Example 1: add and multiply #
The simplest building blocks:
fun add(a, b) {
return a + b;
}
fun multiply(a, b) {
return a * b;
}
{
emit f"3 + 4 = {add(3, 4)}"; // 7
emit f"5 * 6 = {multiply(5, 6)}"; // 30
emit f"add then multiply: {multiply(add(2, 3), 4)}"; // 20
}
Example 2: greet with Default Behavior #
fun greet(name) {
if (name == nil) {
return "Hello, stranger!";
}
return f"Hello, {name}!";
}
{
emit greet("Alice"); // Hello, Alice!
emit greet(nil); // Hello, stranger!
}
Example 3: max and min #
fun max(a, b) {
if (a >= b) {
return a;
}
return b;
}
fun min(a, b) {
if (a <= b) {
return a;
}
return b;
}
fun clamp(value, low, high) {
return max(low, min(value, high));
}
{
emit str(max(10, 20)); // 20
emit str(min(10, 20)); // 10
emit str(clamp(150, 0, 100)); // 100
emit str(clamp(-5, 0, 100)); // 0
emit str(clamp(50, 0, 100)); // 50
}
Example 4: is_even and is_odd #
fun is_even(n) {
return n % 2 == 0;
}
fun is_odd(n) {
return !is_even(n);
}
{
emit "4 is even: " + str(is_even(4)); // true
emit "7 is even: " + str(is_even(7)); // false
emit "7 is odd: " + str(is_odd(7)); // true
}
Example 5: Power (Iterative) #
Recursion is elegant, but sometimes an iterative approach is clearer:
fun power(base, exp) {
let result = 1;
let i = 0;
while (i < exp) {
result = result * base;
i = i + 1;
}
return result;
}
{
emit f"2^10 = {power(2, 10)}"; // 1024
emit f"3^4 = {power(3, 4)}"; // 81
emit f"5^0 = {power(5, 0)}"; // 1
}
Example 6: String Repetition #
fun repeat_string(s, times) {
let result = "";
let i = 0;
while (i < times) {
result = result + s;
i = i + 1;
}
return result;
}
fun draw_line(width) {
return repeat_string("-", width);
}
{
emit draw_line(30);
emit " Neam Function Library ";
emit draw_line(30);
}
Expected output:
------------------------------
Neam Function Library
------------------------------
5.12 The Complete Functions Example #
Here is the full example from examples/03_functions.neam, annotated:
//
// Example 03: Functions
// Demonstrates function declaration and calling
//
// A simple two-parameter function
fun add(a, b) {
return a + b;
}
// A function that returns a formatted string (using f-string)
fun greet(name) {
return f"Hello, {name}!";
}
// A recursive function with a base case
fun factorial(n) {
if (n <= 1) {
return 1;
}
return n * factorial(n - 1);
}
// Main execution block -- where the program starts
{
emit greet("World"); // Hello, World!
emit f"3 + 4 = {add(3, 4)}"; // 3 + 4 = 7
emit f"5! = {factorial(5)}"; // 5! = 120
emit f"10! = {factorial(10)}"; // 10! = 3628800
}
This program demonstrates the three fundamental patterns:
- Simple computation (
add) -- takes inputs, computes, returns a result. - String formatting (
greet) -- builds and returns a formatted string using f-strings. - Recursion (
factorial) -- a function that calls itself with a base case.
5.13 Chapter Summary #
Functions are the primary tool for organizing and reusing code in Neam:
fun name(params) { body }defines a named function.fn(params) { body }creates an anonymous function (lambda/closure). The concise formfn(x) { x * 2 }implicitly returns the last expression.- Functions are defined outside the main block and called from within it.
- Parameters receive values by copy; modifying them does not affect the caller.
returnexits the function with a value; omitting it returnsnil.- Multi-return with tuples lets a function return multiple values:
return (a, b);with the caller usinglet (x, y) = func();. - Destructuring works with function return values for tuples
(
let (x, y) = ...), lists (let [first, ...rest] = ...), and maps (let {name, age} = ...). - Recursive functions call themselves and need a base case to avoid infinite loops.
- Visibility modifiers (
pub,crate,super) control access across modules. - Anonymous functions can capture variables from their surrounding scope (closures).
- Higher-order functions like
map,filter,fold,find,sort_by, andgroup_bytake functions as arguments, enabling powerful data transformation patterns. - The pipe operator (
|>) makes function chains readable by writing them left-to-right instead of inside-out. It desugars at compile time with no runtime overhead. Useexpr |> .method(args)for method calls. - Standard library modules (
std.math,std.crypto, etc.) provide ready-made functions you can import and use immediately. - Callbacks connect functions to agent events -- handoffs, skill calls, and response processing all use functions as their implementation mechanism.
- The call stack tracks function invocations; each call creates a frame that is removed on return.
In the next chapter, you will learn about control flow -- if/else for decisions,
while and for for repetition, break and continue for loop control, and
try/catch for error handling.
Exercises #
Exercise 5.1: Circle Geometry
Write two functions: circle_area(radius) that returns the area of a circle
(pi * r^2, use 3.14159 for pi), and circle_circumference(radius) that returns
the circumference (2 * pi * r). Call both from the main block with radius 5 and
emit the results.
Exercise 5.2: BMI Calculator
Write a function bmi(weight_kg, height_m) that computes the Body Mass Index
(weight / height^2). Then write a function bmi_category(bmi_value) that returns
"Underweight", "Normal", "Overweight", or "Obese" based on standard ranges
(< 18.5, 18.5-24.9, 25-29.9, >= 30). Test with several inputs.
Exercise 5.3: Recursive Sum
Write a recursive function sum_to(n) that returns the sum of all integers from
1 to n. For example, sum_to(5) should return 15 (1+2+3+4+5). What is the base
case?
Exercise 5.4: Fibonacci Comparison
Write two versions of Fibonacci: one recursive (fib_recursive) and one iterative
using a while loop (fib_iterative). Verify they produce the same results for
n = 0 through 10. Which would you use for fib(30) and why?
Exercise 5.5: String Utilities Write the following utility functions:
is_empty(s)-- returnstrueif the string has length 0.starts_with(s, prefix)-- returnstrueifsstarts withprefix. (Hint: uses.substring(0, len(prefix)).)pad_right(s, width)-- returnsspadded with spaces on the right to reach the given width.
Test each function from the main block.
Exercise 5.6: GCD
Write a function gcd(a, b) that computes the greatest common divisor using
the Euclidean algorithm: if b is 0, return a; otherwise return gcd(b, a % b).
Test with gcd(48, 18) (should return 6) and gcd(100, 75) (should return 25).
Exercise 5.7: Function Composition
Write three functions: double(x), increment(x), and square(x). Then write a
function transform(x) that applies all three in sequence: double, then increment,
then square. What is transform(3)? (Should be: double(3)=6, increment(6)=7,
square(7)=49.)
Exercise 5.8: Recursive Power
Write a recursive function power(base, exp) where:
- Base case: if exp is 0, return 1.
- Recursive case: return base * power(base, exp - 1).
Verify that power(2, 10) returns 1024.
Exercise 5.9: Pipe Pipeline
Rewrite the "sum of squares of even numbers" example from Section 5.9 using the
pipe operator. Starting with let data = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10], use
|> to chain filter, map, and fold into a readable pipeline. Verify that
the result is still 220. Then extend the pipeline: before squaring, also filter
out numbers greater than 8. What is the new result?
Exercise 5.10: Stdlib Exploration
Use import std.math::{sqrt, pow} to write a function hypotenuse(a, b) that
computes the hypotenuse of a right triangle using the Pythagorean theorem:
c = sqrt(a^2 + b^2). Test with hypotenuse(3, 4) (should return 5) and
hypotenuse(5, 12) (should return 13). Use f-strings to emit the results:
emit f"hypotenuse(3, 4) = {hypotenuse(3, 4)}".
Exercise 5.11: Multi-Return
Write a function divide_full(a, b) that returns a tuple of three values:
the quotient (a / b), the remainder (a % b), and a boolean indicating
whether the division is exact (remainder is 0). Use tuple destructuring in
the caller to unpack all three values. Test with divide_full(10, 3) and
divide_full(12, 4).
Exercise 5.12: Destructuring Practice
Write three functions:
1. get_rgb() that returns a tuple (255, 128, 0).
2. get_priorities() that returns a list ["critical", "high", "medium", "low"].
3. get_config() that returns a map {"host": "localhost", "port": 8080}.
In the main block, destructure each return value using the appropriate pattern
(let (r, g, b) = ..., let [top, ...rest] = ..., let {host, port} = ...)
and emit the results.
Exercise 5.13: sort_by and group_by Given the following list of students:
let students = [
{"name": "Alice", "grade": "A", "score": 95},
{"name": "Bob", "grade": "B", "score": 82},
{"name": "Carol", "grade": "A", "score": 91},
{"name": "Dave", "grade": "C", "score": 73},
{"name": "Eve", "grade": "B", "score": 88}
];
Use sort_by to sort students by score in ascending order. Then use
group_by to group them by grade. Emit both results.
Exercise 5.14: Lambda Pipeline
Rewrite Exercise 5.9 (the pipe pipeline) using concise lambdas (without
return). Starting with [1, 2, 3, 4, 5, 6, 7, 8, 9, 10], write a pipeline
using |> with filter(fn(x) { x % 2 == 0 }), map(fn(x) { x * x }), and
fold(0, fn(acc, x) { acc + x }). Compare the readability with the verbose
version from Exercise 5.9.