Chapter 7: Functions: Small Pieces, Big Ideas
In Chapter 6, we learned how to control when code runs.
Now we ask the next question:
> “How do we package useful logic so we can call it again?”
That is the job of functions.
Functions are where scripts stop being a pile of steps and start being small systems of reusable ideas.
They also give names to decisions and loops from the last chapter, so the main path through a script can stay readable.
In this chapter we will cover:
- function definition and calling,
- positional, optional, default, typed, and named-style parameters,
- typed return values,
- closures and lexical capture,
- anonymous functions and lambdas,
- recursion.
7.1 Function definition basics
A named function definition looks like this:
// Function definition
function add ( x, y ) {
return x + y;
}
// Function call
say add( 2, 5 ); # 7
A few quick reminders:
- Parameters are listed in parentheses.
- Function bodies are blocks in
{ ... }. returnexits the function and gives back a value.- If you do not return explicitly, the function still returns something
(often
null).
A named function declaration creates a binding in the current scope, so you can call it like any other value.
You can also predeclare a function name before giving it a body. This is useful when two functions need to call each other:
function handle_array;
function handle_dict;
function handle_array ( value ) {
if ( typeof value eq "Dict" ) {
return handle_dict( value );
}
return "array";
}
function handle_dict ( value ) {
if ( typeof value eq "Array" ) {
return handle_array( value );
}
return "dict";
}
After function handle_array;, the name is in scope, but calling it before the full definition is reached throws an exception. Once the definition is evaluated, the placeholder receives its body and behaves like an ordinary function.
Function scoping
Like variables, functions are only defined in a particular scope.
{
function do_it () {
say "Just do it!";
}
do_it(); // works
}
do_it(); // error: no such function
7.2 Positional parameters: the default shape
Most functions start with positional parameters.
function brew_label ( cups, mode ) {
return mode _ ":" _ cups;
}
say brew_label( 2, "cozy" );
Arguments are matched by order:
- first argument -> first parameter,
- second argument -> second parameter,
- and so on.
If a function is called with too many or too few arguments, an error occurs.
7.3 Typed parameters: runtime-checked contracts
SusScript supports type annotations on parameters:
function label_score ( Number n ) {
return `score=${n}`;
}
say label_score( 9 );
If the function is called with a mismatched value it causes a TypeException error.
You can type multiple parameters:
function announce ( String name, Number cups ) {
say `${name} has ${cups} of coffee.`;
}
Type annotations are especially useful at boundaries:
- user input parsing,
- module APIs,
- public helper functions used by many scripts.
They make failures earlier and clearer. If a function is just a little helper used in one place, it may be less important to check parameter types.
7.4 Optional and default parameters
You can make trailing parameters optional with ?:
function mood_line ( mood, label? ) {
if ( label ≡ null ) {
return "(no label) " _ mood;
}
return label _ ": " _ mood;
}
say mood_line( "sleepy" );
say mood_line( "sleepy", "Zia" );
You can also provide defaults with :=:
function tea_or_coffee (
String name,
String drink := "coffee"
) {
return name _ " picks " _ drink;
}
say tea_or_coffee( "Zia" );
say tea_or_coffee( "Zenia", "tea" );
Inside a function, __argc__ is a const value containing the number of positional arguments that were actually supplied to the current call. That matters when an optional argument can be omitted or explicitly passed as null:
function describe_label ( label? ) {
if ( __argc__ = 0 ) {
return "no argument was supplied";
}
if ( label ≡ null ) {
return "argument was explicit null";
}
return "label: " _ label;
}
say describe_label(); // no argument was supplied
say describe_label(null); // argument was explicit null
say describe_label("Zia"); // label: Zia
Most functions do not need __argc__. It is there for APIs where the difference between "not provided" and "provided as null" is part of the contract.
Ordering rule (important)
Once a parameter is optional (?) or has a default (:=), following parameters cannot be required.
Good:
function ok ( a, b?, c := 3 ) {
return a;
}
Not allowed:
function not_ok ( a?, b ) {
return a;
}
7.5 Variadic parameters, named-style arguments, and argument spread
Sometimes you want to accept additional arguments.
Positional rest collector
In a parameter list, use ... with a collector name:
function add_all ( ... rest ) {
let total := 0;
for ( let n in rest ) {
total += n;
}
return total;
}
say add_all( 1, 2, 3, 4 ); # 10
The rest variable will be an Array that collects any additional positional arguments.
Named argument collection
ZuzuScript call syntax supports key–value-style arguments. To receive these, define a collector but give it type PairList:
function describe_job ( name, ... PairList opts ) {
return name _ " options=" _ opts.length();
}
say describe_job(
"build",
mode: "release",
drink: "coffee"
);
You can think of this as a practical named-parameter pattern:
- caller writes labeled arguments,
- function receives collected key/value pairs.
It is a flexible pattern for options and evolving APIs.
It is possible to define both kinds of collectors for the dame function:
function do_stuff ( ... Array args, PairList opts ) {
...;
}
do_stuff( opt1: true, "arg1", opt2: false, "arg2", "arg3" );
Argument spread at call sites
In a call argument list, ...expr is argument spread. It evaluates expr and expands the resulting collection at that point in the call:
function positional ( ... args ) {
return args;
}
say positional( 1, ...[ 2, 3 ], 4 ); // [ 1, 2, 3, 4 ]
Array spread appends positional arguments. Dict and PairList spread append named arguments:
function capture ( ... PairList opts ) {
return opts;
}
let from_dict := capture( before: 0, ...{ alpha: 1 }, after: 2 );
say from_dict{"alpha"}; // 1
let from_pairs := capture( ...{{ tag: "one", tag: "two" }} );
say from_pairs.get_all("tag"); // [ "one", "two" ]
Argument operands evaluate left-to-right, with each spread expanded in place. PairList spread preserves pair order and duplicate keys. Dict spread supplies named arguments for its entries, but code should not rely on Dict iteration order. Spreading any value other than Array, Dict, or PairList throws an exception.
Keep the three ... meanings separate:
function f ( ... args )collects extra positional arguments,f( ...values )spreads a collection into a call,[ 1 ... 5 ]is an inclusive range inside collection literals.
7.6 Return values and return types
So far we have returned values informally. You can also declare a return type using → (or the more keyboard-friendly ->):
function double ( Number n ) → Number {
return n × 2;
}
If a returned value does not match the declared type, runtime raises a type error.
function bad_label () → Number {
return "oops";
}
That contract applies whether returning from:
- the end of the function,
- an early
returninside a branch.
Why return types help
Return types answer: “What comes back from this function?”
That improves:
- call-site confidence,
- maintenance safety,
- tooling opportunities.
For small local helpers they may be optional, but for shared APIs they are often worth adding.
7.7 Closures: functions that remember
A closure is a function value that captures variables from its lexical surroundings.
function make_counter ( start := 0 ) {
let current := start;
return function () {
current := current + 1;
return current;
};
}
let next_id := make_counter( 40 );
say next_id(); # 41
say next_id(); # 42
Even after make_counter returns, the inner function keeps access to current. That is lexical capture in action.
Practical closure uses
Closures are great for:
- stateful callbacks,
- tiny configurable helpers,
- memoization-like caches,
- dependency injection without classes.
7.8 Anonymous functions and fn lambdas
You have two common expression forms for unnamed callables.
Anonymous function expression
let square := function ( n ) {
return n × n;
};
say square( 6 );
Use this when you want a full block body.
fn lambda shorthand
The fn keyword is an abbreviated way of writing anonymous functions. The example above could be written as:
let square := fn n → n × n; say square( 6 );
fn is handy for short expression-like callbacks:
let nums := [ 1, 2, 3, 4 ]; let doubled := nums.map( fn n → n * 2 ); say doubled;
For the shortest single-argument callbacks, start with an arrow. This creates a lambda whose optional argument is available as ^^:
let square := → ^^ × ^^; say square( 6 );
The ^^ parameter is implicit in the leading-arrow form. It is not valid in an explicit fn parameter list.
You can also use block-like side effects when collection APIs expect a callback:
let total := 0; [ 1, 2, 3 ].each( → total := total + ^^ ); say total; # 6
A practical rule:
- use
fnfor short, local callback intent, - use
functionwhen logic grows beyond one simple expression/step.
The ultra-abbreviated form using the leading → and ^^ is best when it already obvious from context that an anonymous function is expected, such as in the .each example above.
7.9 Recursion: functions calling themselves
Recursion is useful when a problem is naturally "same shape, smaller input."
Classic example:
function factorial ( Number n ) -> Number {
if ( n ≤ 1 ) {
return 1;
}
return n × factorial( n - 1 );
}
say factorial( 5 ); # 120
Another beginner-friendly pattern: walking nested data.
function count_nodes ( item ) → Number {
return 0 if item ≡ null;
if ( typeof item eq "Array" ) {
let total := 1;
for ( const child in item ) {
total := total + count_nodes( child );
}
return total;
}
return 1;
}
Recursion safety checklist
When writing recursive functions, verify three things:
- Base case exists (stops recursion),
- Recursive step progresses toward base case,
- Return type stays consistent across all paths.
If any of these are missing, recursion becomes an infinite loop.
7.10 Function design patterns you will use constantly
Functions are not just syntax; they are design choices.
Here are practical patterns that work well in ZuzuScript:
Pattern A: Validate early, return early
function parse_cups ( raw ) -> Number {
return 0 if raw ≡ null;
return 0 if raw eq "";
return raw + 0;
}
Keeps the “normal path” at the bottom and shallow.
Pattern B: Keep one job per function
function fetch_feed ( url ) { ... }
function parse_feed ( text ) { ... }
function render_feed ( items ) { ... }
Small, focused functions are easier to test and reuse.
Pattern C: Name by outcome, not mechanism
Prefer build_report over loop_and_concat_stuff.
Future-you (and teammates) will thank you.
Pattern D: Stabilize boundaries with types
For public or reused helpers:
- type key parameters,
- add return types for non-trivial outputs,
- use defaults for optional behaviour.
This creates contracts that scale as code grows.
7.11 Mini walkthrough: callback pipeline for sleepy tasks
Let’s combine typed params, defaults, closures, and lambdas.
function make_priority_filter ( Number min_priority := 5 ) {
return fn task → task{priority} ≥ min_priority;
}
function pick_titles ( Array tasks, Function keep ) → Array {
let out := [];
for ( const t in tasks ) {
next unless keep(t);
out.push( t{name} );
}
return out;
}
let tasks := [
{ name: "lint", priority: 3 },
{ name: "docs", priority: 7 },
{ name: "ship", priority: 9 },
];
let keep_important := make_priority_filter(7);
let picks := pick_titles( tasks, keep_important );
say picks; # [ "docs", "ship" ]
What this demonstrates:
make_priority_filterreturns a closure capturingmin_priority,pick_titlesaccepts a function as a parameter,- callback-driven logic stays reusable and composable.
This style appears all over real scripts (build tooling, filters, reporting jobs, CLI data transforms).
7.12 What comes next
You now know how to package behaviour and pass it around as values.
In Chapter 8, we move from function-level structure to object-level structure:
- classes and methods,
- traits/roles,
- encapsulation,
- composition patterns.
If functions are excellent tools, objects are the toolbox.