Programming Neam
📖 17 min read

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.

💠 Why This Matters

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:

🌎 Real-World Analogy: Functions Are Recipes

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 #

💠 Why This Matters

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:

neam
fun greet(name) {
  return "Hello, " + name + "!";
}

{
  emit greet("World");
  emit greet("Neam");
  emit greet("Alice");
}

Expected output:

text
Hello, World!
Hello, Neam!
Hello, Alice!
💡 Tip

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:

text
fun <name>(<param1>, <param2>, ...) {
  // body
  return <value>;
}

Key rules:

Where Functions Live #

This is the standard layout of a Neam program:

neam
// 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.

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

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

neam
fun get_greeting() {
  return "Hello, World!";
}

{
  emit get_greeting();
}

Functions with Multiple Parameters #

There is no fixed limit on the number of parameters:

neam
fun create_full_name(first, middle, last) {
  return f"{first} {middle} {last}";
}

{
  emit create_full_name("Ada", "Augusta", "Lovelace");
}
🎯 Try It Yourself #1

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 #

💠 Why This Matters

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:

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

neam
fun say_hello(name) {
  emit "Hello, " + name + "!";
  // No return statement -- returns nil
}

{
  let result = say_hello("Bob");
  emit "Returned: " + str(result);
}

Expected output:

text
Hello, Bob!
Returned: nil
Common Mistake: Forgetting `return`

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.

neam
// 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:

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

neam
// 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:

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

neam
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.

Call Stack (grows up)

Each function call creates a stack frame that holds:

  1. The function's local variables and parameters
  2. 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:

In Neam:

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

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

  1. A base case that stops the recursion. Without it, the function would call itself forever and eventually crash with a stack overflow.
  2. 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, ...

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

neam
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):

text
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.

🎯 Try It Yourself #2

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:

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

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

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

neam
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 Patterns at a Glance
TUPLE let (x, y) = get_point();
Unpacks positional values
LIST let [first, ...rest] = get_items();
Captures head + remaining elements
MAP let {name, age} = get_person();
Extracts values by matching key names
💡 Tip

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 #

neam
// 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;
}
neam
// 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:

neam
module myproject::internal;

crate fun shared_secret() {
  return "only within this project";
}

Super Visibility #

super makes a function visible to the parent module:

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


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.

📝 `fun` vs `fn` rule

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.

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:

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

neam
{
  // 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:

neam
{
  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:

neam
{
  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:

neam
{
  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:

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

neam
{
  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:

neam
{
  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:

neam
{
  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:

neam
{
  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:

neam
{
  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:

neam
{
  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:

neam
{
  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:

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

neam
// 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:

neam
{
  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:

neam
{
  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:

text
// 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:

neam
{
  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:

neam
{
  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.

🎯 Try It Yourself #3

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:

neam
// 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:

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

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

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

neam
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 #

neam
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 #

neam
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 #

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

neam
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 #

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

text
------------------------------
  Neam Function Library
------------------------------

5.12 The Complete Functions Example #

Here is the full example from examples/03_functions.neam, annotated:

neam
//
// 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:

  1. Simple computation (add) -- takes inputs, computes, returns a result.
  2. String formatting (greet) -- builds and returns a formatted string using f-strings.
  3. 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:

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:

  1. is_empty(s) -- returns true if the string has length 0.
  2. starts_with(s, prefix) -- returns true if s starts with prefix. (Hint: use s.substring(0, len(prefix)).)
  3. pad_right(s, width) -- returns s padded 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:

neam
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.

Start typing to search...