Chapter 6: Functions: Small Pieces, Big Ideas
In Chapter 5, we learned how to control when code runs.
Now we ask the next cozy 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.
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.
As with earlier chapters, examples here follow the Perl implementation and align with parser/runtime behaviour used in tests.
6.1 Function definition basics
A named function definition looks like this:
function add ( x, y ) {
return x + y;
}
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 (often
nullunless your body computes and returns via control semantics).
A named function declaration creates a binding in the current scope, so you can call it like any other value.
6.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 the arity does not match the function contract, runtime errors are raised (for example, too many or too few arguments when no variadic collector is present).
Readability tip
Try to keep positional argument order “obvious”:
- data first,
- options later,
- avoid signatures like
( a, b, c, flag, mode, maybe, x ).
If a signature grows awkward, split into helper functions or use named-style collection (covered below).
6.3 Typed parameters: runtime-checked contracts
The Perl implementation supports type annotations on parameters:
function label_score ( Number n ) {
return `score=${n}`;
}
say label_score( 9 );
If a call provides a mismatched value, runtime type checking raises a TypeException.
You can type multiple parameters:
function announce ( String name, Number cups ) {
return name _ " has " _ cups _ " 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.
6.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( "Zia", "tea" );
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;
}
That ordering rule is parser-enforced in the Perl implementation.
6.5 Variadic parameters and “named-style” arguments
Sometimes you want to accept additional arguments.
Positional rest collector
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
In the Perl runtime, rest is an array-like value.
Named argument collection
ZuzuScript call syntax supports label-style arguments such as key: value. To receive these, define a PairList collector:
function describe_raccoon ( name, ... PairList opts ) {
return name _ " options=" _ opts.size();
}
say describe_raccoon(
"Zia",
mood: "sleepy",
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 flexible for option bags and evolving APIs.
6.6 Return values and return types
So far we have returned values informally. You can also declare a return type using -> (or Unicode →):
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.
6.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 += 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.
If Chapter 3’s scope/lifetime section felt abstract, closures are where it becomes concrete.
6.8 Anonymous functions and fn lambdas
You have two common expression forms for unnamed callables.
Anonymous function expression
let square := function ( x ) {
return x * x;
};
say square( 6 );
Use this when you want a full block body.
fn lambda shorthand
fn is handy for short expression-like callbacks:
let nums := [ 1, 2, 3, 4 ]; let doubled := nums.map( fn x -> x * 2 ); say doubled;
You can also use block-like side effects when collection APIs expect a callback:
let total := 0; [ 1, 2, 3 ].each( fn x -> total := total + x ); say total; # 6
A practical rule:
- use
fnfor short, local callback intent, - use
functionwhen logic grows beyond one simple expression/step.
Both are first-class values.
6.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 {
if ( item = null ) {
return 0;
}
if ( typeof item = "Array" ) {
let total := 1;
for ( let child in item ) {
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 nap loop. (Adorable, but not productive.)
6.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 {
if ( raw = null ) {
return 0;
}
if ( raw = "" ) {
return 0;
}
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.
6.11 Mini walkthrough: callback pipeline for sleepy tasks
Let’s combine typed params, defaults, closures, and lambdas.
function make_filter ( Number min_priority := 5 ) {
return fn task -> task{priority} >= min_priority;
}
function pick_titles ( tasks, keep ) -> Array {
let out := [ ];
for ( let t in tasks ) {
if ( not keep( t ) ) {
next;
}
out.push( t{name} );
}
return out;
}
let tasks := [
{ name: "lint", priority: 3 },
{ name: "docs", priority: 7 },
{ name: "ship", priority: 9 },
];
let keep_important := make_filter( 7 );
let picks := pick_titles( tasks, keep_important );
say picks; # [ "docs", "ship" ]
What this demonstrates:
make_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).
6.12 What comes next
You now know how to package behaviour and pass it around as values.
In Chapter 7, 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. Zia is currently sleeping inside that toolbox.