Chapter 7: Collections #
How to create, access, and manipulate every collection type in Neam -- List, Map, Set, Tuple, Range, TypedArray, Record, and Table. How to iterate over collections using loops and the lazy iterator protocol. How to build nested data structures. How to use slicing, the spread operator, broadcasting, and the pipe operator for fluent data pipelines. How to apply practical patterns -- accumulation, filtering, transformation, and aggregation -- that form the backbone of real-world agent data processing.
Why Collections Matter #
Think about what an AI agent actually does moment to moment. It receives a query, fetches documents from a knowledge base, scores them for relevance, discards duplicates, ranks the survivors, and assembles a response. Every one of those steps is an operation on a collection of data.
A customer-service agent manages a list of conversation messages. A research agent stores query results in maps keyed by source URL. A deduplication step needs a set to strip out repeated entries in constant time. A function returns a coordinate pair -- an (x, y) tuple -- because creating a full map for two values is overkill. An embedding pipeline stores 768 float values in a typed array for vectorized math. A data-analysis agent reshapes rows and columns in a table. Without collections, agents cannot accumulate context, store tool results, or coordinate across multiple steps.
Collections are the bloodstream of every agent system. Master them and you can build agents that process, transform, and reason over real-world data fluently.
Neam provides ten collection-related constructs:
Both lists and maps are dynamically typed: a single list can hold strings, numbers, booleans, other lists, and maps simultaneously. This chapter covers creation, access, iteration, and the practical patterns you will use every day.
Lists #
Creating a List #
A list literal is a comma-separated sequence of expressions enclosed in square brackets:
{
let fruits = ["apple", "banana", "cherry"];
emit "Fruits: " + str(fruits);
}
Output:
Fruits: [apple, banana, cherry]
Lists can contain any mix of types:
{
let mixed = ["hello", 42, true, 3.14, nil];
emit "Mixed: " + str(mixed);
}
An empty list is created with []:
{
let empty = [];
emit "Empty list: " + str(empty);
emit "Length: " + str(len(empty));
}
Output:
Empty list: []
Length: 0
Indexing #
Lists are zero-indexed. Use square bracket notation to access an element by its position:
{
let fruits = ["apple", "banana", "cherry", "date"];
emit "First fruit: " + fruits[0];
emit "Second fruit: " + fruits[1];
emit "Last fruit: " + fruits[3];
}
Output:
First fruit: apple
Second fruit: banana
Last fruit: date
Accessing an index beyond the length of the list causes a runtime error.
Always check len() before accessing by computed index when the index is not guaranteed
to be in range.
List Length #
The built-in function len() returns the number of elements in a list:
{
let fruits = ["apple", "banana", "cherry", "date"];
emit "Number of fruits: " + str(len(fruits));
}
Output:
Number of fruits: 4
Modifying Lists #
Use push() to append an element to the end of a list:
{
let colors = ["red", "green"];
push(colors, "blue");
emit "Colors: " + str(colors);
emit "Length: " + str(len(colors));
}
Output:
Colors: [red, green, blue]
Length: 3
You can also assign to a specific index to replace an element:
{
let scores = [90, 85, 78];
scores[1] = 92;
emit "Updated scores: " + str(scores);
}
Output:
Updated scores: [90, 92, 78]
Iterating Over a List with While #
Neam uses while loops for iteration. The standard pattern is an index variable that
increments from 0 up to len():
{
let fruits = ["apple", "banana", "cherry", "date"];
let i = 0;
while (i < len(fruits)) {
emit " " + str(i + 1) + ". " + fruits[i];
i = i + 1;
}
}
Output:
1. apple
2. banana
3. cherry
4. date
When iterating with while, always ensure the loop variable is incremented
inside the loop body. Forgetting i = i + 1; produces an infinite loop.
Iterating with for-in Loops #
While the while loop works, Neam provides a more concise for-in loop for iterating
over collections. The for-in loop handles indexing automatically:
{
let fruits = ["apple", "banana", "cherry", "date"];
for (fruit in fruits) {
emit " - " + fruit;
}
}
Output:
- apple
- banana
- cherry
- date
When you need both the index and the value, use enumerate():
{
let fruits = ["apple", "banana", "cherry", "date"];
for (i, fruit in enumerate(fruits)) {
emit " " + str(i + 1) + ". " + fruit;
}
}
Output:
1. apple
2. banana
3. cherry
4. date
You can also iterate a fixed number of times using range():
{
// Print a simple multiplication table for 5
for (i in range(1, 6)) {
emit "5 x " + str(i) + " = " + str(5 * i);
}
}
Output:
5 x 1 = 5
5 x 2 = 10
5 x 3 = 15
5 x 4 = 20
5 x 5 = 25
| Loop Type | Best For |
|---|---|
for (item in list) |
Simple iteration over all elements |
for (i, item in enumerate(list)) |
When you need the index and value |
for (i in range(start, end)) |
Counted iteration, numeric sequences |
while (condition) |
Complex conditions, early exit, unknown length |
Advanced List Operations #
Beyond push() and indexing, Neam provides a rich set of list operations:
pop() -- Remove the Last Element
The pop() function removes and returns the last element of a list:
{
let stack = ["first", "second", "third"];
let top = pop(stack);
emit "Popped: " + top;
emit "Remaining: " + str(stack);
}
Output:
Popped: third
Remaining: [first, second]
This makes lists usable as stacks (last-in, first-out).
concat() -- Combine Two Lists
The concat() function joins two lists into a new list:
{
let part1 = ["a", "b", "c"];
let part2 = ["d", "e", "f"];
let combined = concat(part1, part2);
emit "Combined: " + str(combined);
emit "Original part1: " + str(part1); // Unchanged
}
Output:
Combined: [a, b, c, d, e, f]
Original part1: [a, b, c]
Note that concat() returns a new list -- the originals are not modified.
reverse() -- Reverse a List
{
let letters = ["a", "b", "c", "d"];
let reversed = reverse(letters);
emit "Reversed: " + str(reversed);
}
Output:
Reversed: [d, c, b, a]
sort() -- Sort a List
The sort() function sorts a list in ascending order:
{
let scores = [78, 95, 62, 88, 45];
let sorted_scores = sort(scores);
emit "Sorted: " + str(sorted_scores);
}
Output:
Sorted: [45, 62, 78, 88, 95]
find() -- Find an Element
The find() function searches a list using a predicate function and returns the first
matching element, or nil if no match is found:
{
let numbers = [3, 7, 12, 5, 18, 9];
let first_big = find(numbers, fn(n) { return n > 10; });
emit "First number > 10: " + str(first_big);
}
Output:
First number > 10: 12
Quick Reference: List Operations
| Operation | Syntax | Returns |
|---|---|---|
| Create | [a, b, c] |
New list |
| Access | list[i] |
Element at index |
| Length | len(list) |
Number of elements |
| Append | push(list, val) |
Modifies in place |
| Remove last | pop(list) |
Removed element |
| Combine | concat(a, b) |
New list |
| Reverse | reverse(list) |
New list |
| Sort | sort(list) |
New sorted list |
| Find | find(list, fn) |
First match or nil |
List Methods #
Neam provides a rich set of method-style operations on lists. These methods are called with dot notation on the list itself, making code more readable and chainable.
Statistical Methods
When working with numeric data -- scores, prices, sensor readings -- you often need quick summaries:
{
let scores = [85, 92, 78, 95, 88, 91, 76, 89];
emit f"Sum: {scores.sum()}"; // 694
emit f"Mean: {scores.mean()}"; // 86.75
emit f"Min: {scores.min()}"; // 76
emit f"Max: {scores.max()}"; // 95
emit f"Count: {scores.count()}"; // 8
}
Output:
Sum: 694
Mean: 86.75
Min: 76
Max: 95
Count: 8
Sorting, Uniqueness, and Membership
{
let data = [5, 3, 1, 4, 2, 3, 5, 1];
emit f"Sorted: {data.sort()}"; // [1, 1, 2, 3, 3, 4, 5, 5]
emit f"Unique: {data.unique()}"; // [5, 3, 1, 4, 2]
emit f"Contains: {data.contains(4)}"; // true
emit f"Empty?: {data.is_empty()}"; // false
emit f"First: {data.first()}"; // 5
emit f"Last: {data.last()}"; // 1
}
The sort() function from earlier and the .sort() method behave the same way.
The method form is preferred because it chains naturally with other methods.
sort_by() -- Custom Sort Order
When sorting complex data, use sort_by() with a key function:
{
let agents = [
{"name": "Researcher", "priority": 3},
{"name": "Router", "priority": 1},
{"name": "Writer", "priority": 2}
];
let ordered = agents.sort_by(fn(a) { return a.priority; });
for (a in ordered) {
emit f"{a.priority}. {a.name}";
}
}
Output:
1. Router
2. Writer
3. Researcher
Zip and Enumerate
zip() pairs up elements from two lists, and enumerate() pairs each element with its
index. Both are invaluable for parallel iteration:
{
let names = ["Alice", "Bob", "Carol"];
let ages = [30, 25, 35];
for (pair in names.zip(ages)) {
emit f"{pair.0} is {pair.1}";
}
for (item in names.enumerate()) {
emit f"{item.0}: {item.1}";
}
}
Output:
Alice is 30
Bob is 25
Carol is 35
0: Alice
1: Bob
2: Carol
Flatten, FlatMap, Chunk, and Window
These methods reshape lists in ways that come up in real agent pipelines -- flattening nested results, batching items for parallel processing, and sliding-window analysis:
{
// Flatten nested lists into one
emit [[1, 2], [3, 4], [5, 6]].flatten(); // [1, 2, 3, 4, 5, 6]
// flat_map: map then flatten in one step
let words = ["hello world", "good morning"];
emit words.flat_map(fn(s) { return split(s, " "); });
// ["hello", "world", "good", "morning"]
// Split a list into chunks of a given size
emit [1, 2, 3, 4, 5, 6, 7].chunk(3); // [[1,2,3], [4,5,6], [7]]
// Sliding window of size n
emit [1, 2, 3, 4, 5].window(3);
// [[1,2,3], [2,3,4], [3,4,5]]
}
Take, Skip, Any, All
{
let nums = [10, 20, 30, 40, 50];
emit nums.take(3); // [10, 20, 30]
emit nums.skip(2); // [30, 40, 50]
emit nums.any(fn(n) { return n > 40; }); // true
emit nums.all(fn(n) { return n > 5; }); // true
}
group_by() -- Group Elements by Key
{
let logs = [
{"level": "info", "msg": "started"},
{"level": "error", "msg": "timeout"},
{"level": "info", "msg": "connected"},
{"level": "error", "msg": "refused"}
];
let grouped = logs.group_by(fn(e) { return e.level; });
emit f"Info count: {len(grouped["info"])}"; // 2
emit f"Error count: {len(grouped["error"])}"; // 2
}
Join and Conversion
{
// Join list elements into a string
emit ["Neam", "is", "awesome"].join(" "); // "Neam is awesome"
// Convert to a set (removes duplicates)
let tags = ["ai", "ml", "ai", "neam"];
let unique_tags = tags.to_set();
emit f"Unique tags: {unique_tags}"; // {ai, ml, neam}
}
You have a list of exam scores:
let exams = [72, 88, 65, 91, 84, 77, 95, 60];. Use the list methods to find the mean
score, the highest score, and produce a sorted version. Then use .unique() on
[1, 2, 2, 3, 3, 3, 4] and see what you get. Experiment in the Neam REPL!
Complete List Methods Reference
| Method | Syntax | Returns |
|---|---|---|
slice |
list.slice(start, end) |
New sub-list |
flatten |
list.flatten() |
Flattened one level |
flat_map |
list.flat_map(fn) |
Map then flatten |
zip |
list.zip(other) |
List of paired tuples |
enumerate |
list.enumerate() |
List of (index, element) tuples |
take |
list.take(n) |
First n elements |
skip |
list.skip(n) |
All but first n elements |
any |
list.any(fn) |
true if any element matches |
all |
list.all(fn) |
true if all elements match |
count |
list.count() |
Number of elements |
group_by |
list.group_by(fn) |
Map of key to sub-lists |
unique |
list.unique() |
New list, duplicates removed |
chunk |
list.chunk(n) |
List of sub-lists of size n |
window |
list.window(n) |
Sliding windows of size n |
sort_by |
list.sort_by(fn) |
Sorted by key function |
min |
list.min() |
Smallest element |
max |
list.max() |
Largest element |
sum |
list.sum() |
Sum of numeric elements |
mean |
list.mean() |
Average of numeric elements |
join |
list.join(sep) |
String with separator |
to_set |
list.to_set() |
Convert to Set |
contains |
list.contains(val) |
true if val is in list |
first |
list.first() |
First element or nil |
last |
list.last() |
Last element or nil |
is_empty |
list.is_empty() |
true if list has no elements |
List Slicing #
Neam supports Python-style list slicing for extracting sub-lists. Slicing never modifies the original list -- it always returns a new one.
The syntax is list[start:end:step], where all three parts are optional:
+-------------------------------------------------------------------+
| Slice Syntax Meaning |
+-------------------------------------------------------------------+
| list[start:end] Elements from start up to (not including) end |
| list[:end] From the beginning up to end |
| list[start:] From start to the end |
| list[::step] Every step-th element |
| list[::-1] Reversed list |
| list[-2:] Last two elements (negative index) |
+-------------------------------------------------------------------+
{
let items = [10, 20, 30, 40, 50, 60, 70, 80];
emit items[2:5]; // [30, 40, 50]
emit items[:3]; // [10, 20, 30]
emit items[5:]; // [60, 70, 80]
emit items[::2]; // [10, 30, 50, 70] (every 2nd)
emit items[::-1]; // [80, 70, 60, 50, 40, 30, 20, 10] (reversed)
// Negative indices count from the end
emit items[-2:]; // [70, 80] (last two)
emit items[-3:]; // [60, 70, 80] (last three)
emit items[1:3]; // [20, 30]
}
Output:
[30, 40, 50]
[10, 20, 30]
[60, 70, 80]
[10, 30, 50, 70]
[80, 70, 60, 50, 40, 30, 20, 10]
[70, 80]
[60, 70, 80]
[20, 30]
Slicing is like tearing pages out of a notebook. You say "give me pages 3 through 5" and you get a copy of those pages -- the original notebook is untouched. The step parameter is like saying "give me every other page."
The Pipe Operator with Collections #
The pipe operator |> lets you chain collection operations into a readable left-to-right
pipeline. Instead of nesting function calls inside each other (which reads inside-out), the
pipe operator lets each step flow naturally into the next.
Think of it like an assembly line: raw data enters on the left, each |> is a station that
transforms it, and the finished product emerges on the right.
{
let data = [5, 3, 1, 4, 2, 3, 5, 1];
// Chain operations fluently
let result = data
|> .unique()
|> .sort()
|> .reverse()
|> .take(3);
emit f"Top 3: {result}"; // [5, 4, 3]
// Real-world: process agent results
let results = [
{"title": "Doc A", "score": 0.95},
{"title": "Doc B", "score": 0.42},
{"title": "Doc C", "score": 0.88}
];
let top_titles = results
|> filter(fn(r) { return r.score > 0.7; })
|> map(fn(r) { return r.title; });
emit f"Relevant: {top_titles}";
}
Output:
Top 3: [5, 4, 3]
Relevant: [Doc A, Doc C]
The pipe operator is especially powerful when combined with list methods. Compare these two equivalent pieces of code:
// Without pipe (nested, reads inside-out):
let result = reverse(sort(unique(data)));
// With pipe (linear, reads left-to-right):
let result = data |> .unique() |> .sort() |> .reverse();
The second version is easier to read, easier to modify (just add or remove a step), and mirrors how you think about data transformations: "take the data, deduplicate it, sort it, then reverse it."
Start with let words = ["banana", "apple", "cherry", "apple", "banana", "date"];.
Use the pipe operator to deduplicate the words, sort them alphabetically, and join them
into a comma-separated string. What do you get?
Maps #
Creating a Map #
A map literal uses curly braces with key-value pairs separated by colons. String keys are the most common, but Neam also supports non-string keys -- any hashable value (numbers, booleans, tuples) can serve as a key:
{
let person = {
"name": "Alice",
"age": 30,
"city": "London"
};
emit "Person: " + str(person);
}
Output:
Person: {name: Alice, age: 30, city: London}
An empty map is created with {}:
{
let config = {};
emit "Empty map: " + str(config);
}
Non-String Keys
Maps accept any hashable value as a key -- numbers, booleans, and tuples all work:
{
// Numeric keys
let http_codes = {200: "OK", 404: "Not Found", 500: "Server Error"};
emit http_codes[200]; // OK
// Tuple keys (useful for coordinate lookups)
let grid = {
(0, 0): "origin",
(1, 0): "east",
(0, 1): "north"
};
emit grid[(0, 0)]; // origin
emit grid[(1, 0)]; // east
}
Map Key Types
+---------------------------------------------------+
| String keys {"name": "Alice"} most common |
| Number keys {1: "one", 2: "two"} |
| Tuple keys {(0,0): "origin"} coordinates |
| Boolean keys {true: "yes"} rare |
+---------------------------------------------------+
Any hashable, immutable value can be a map key.
Lists and maps are NOT hashable (mutable).
Accessing Values #
Use square bracket notation with a string key:
{
let person = {
"name": "Alice",
"age": 30,
"city": "London"
};
emit "Name: " + person["name"];
emit "Age: " + str(person["age"]);
}
Output:
Name: Alice
Age: 30
You can also use dot notation for keys that are valid identifiers:
{
let person = {
"name": "Alice",
"age": 30
};
emit "Name: " + person.name;
emit "Age: " + str(person.age);
}
Adding and Updating Entries #
Assign to a new key to add an entry. Assign to an existing key to update it:
{
let settings = {
"theme": "dark",
"font_size": 14
};
// Add a new key
settings["language"] = "en";
// Update an existing key
settings["font_size"] = 16;
emit "Theme: " + settings["theme"];
emit "Font size: " + str(settings["font_size"]);
emit "Language: " + settings["language"];
}
Output:
Theme: dark
Font size: 16
Language: en
Checking for Keys #
Accessing a key that does not exist with [] throws a runtime error. To safely
check for the presence of a key, use has_key(), get_or(), or the .get()
method (which returns an Option -- see Chapter 8):
{
let person = {"name": "Alice"};
// Safe: check with has_key()
if (!has_key(person, "email")) {
emit "No email on file.";
}
// Safe: provide a default with get_or()
let email = person.get_or("email", "none@example.com");
emit f"Email: {email}"; // Email: none@example.com
// person["email"] --> ERROR: key not found (throws if key is absent)
// Use [] only when you KNOW the key exists.
}
Output:
No email on file.
You can also use the has_key() function for a more explicit check:
{
let person = {"name": "Alice", "age": 30};
if (has_key(person, "name")) {
emit "Name found: " + person.name;
}
if (!has_key(person, "email")) {
emit "No email on file.";
}
}
Iterating Over Maps with for-in #
You can iterate over a map's keys using for-in:
{
let config = {
"provider": "openai",
"model": "gpt-4o",
"temperature": "0.7"
};
for (key in config) {
emit key + " = " + str(config[key]);
}
}
Output:
provider = openai
model = gpt-4o
temperature = 0.7
Advanced Map Operations #
keys() and values() -- Extract Keys or Values
The keys() function returns a list of all keys in a map, and values() returns a list
of all values:
{
let person = {"name": "Alice", "age": 30, "city": "London"};
let all_keys = keys(person);
let all_values = values(person);
emit "Keys: " + str(all_keys);
emit "Values: " + str(all_values);
}
Output:
Keys: [name, age, city]
Values: [Alice, 30, London]
contains() -- Check if a Key Exists
The contains() function returns true if the map has the given key:
{
let settings = {"theme": "dark", "language": "en"};
emit "Has theme: " + str(contains(settings, "theme"));
emit "Has font: " + str(contains(settings, "font"));
}
Output:
Has theme: true
Has font: false
remove() -- Remove a Key
The remove() function removes a key-value pair from a map:
{
let data = {"a": 1, "b": 2, "c": 3};
remove(data, "b");
emit "After remove: " + str(data);
}
Output:
After remove: {a: 1, c: 3}
size() -- Count Entries
The size() function returns the number of key-value pairs:
{
let config = {"model": "gpt-4o", "temperature": 0.7};
emit "Entries: " + str(size(config));
}
Output:
Entries: 2
entries() -- Get Key-Value Pairs
The entries() function returns a list of [key, value] pairs, useful for iteration:
{
let scores = {"Alice": 95, "Bob": 87, "Carol": 92};
for (entry in entries(scores)) {
emit entry[0] + " scored " + str(entry[1]);
}
}
Output:
Alice scored 95
Bob scored 87
Carol scored 92
Quick Reference: Map Functions
| Operation | Syntax | Returns |
|---|---|---|
| Create | {"key": value} |
New map |
| Access | map["key"] or map.key |
Value (throws if key missing) |
| Set | map["key"] = val |
Modifies in place |
| Keys | keys(map) |
List of keys |
| Values | values(map) |
List of values |
| Check key | contains(map, key) |
Boolean |
| Remove | remove(map, key) |
Modifies in place |
| Size | size(map) |
Number of entries |
| Entries | entries(map) |
List of [key, value] pairs |
Map Methods #
Beyond the built-in functions above, maps support a full set of dot-notation methods for safe access, transformation, and querying.
Safe Access: get_or() and get_or_insert()
{
let config = {"model": "gpt-4o", "temperature": 0.7};
// get_or: return a default if key is missing (does not modify the map)
let timeout = config.get_or("timeout", 30);
emit f"Timeout: {timeout}"; // 30
emit f"Keys: {keys(config)}"; // [model, temperature] -- unchanged
// get_or_insert: insert the default if key is missing, then return it
let max_tokens = config.get_or_insert("max_tokens", 4096);
emit f"Max tokens: {max_tokens}"; // 4096
emit f"Keys: {keys(config)}"; // [model, temperature, max_tokens] -- added!
}
merge() -- Combine Two Maps
The merge() method creates a new map with entries from both maps. When keys conflict,
the values from the argument map win:
{
let defaults = {"model": "gpt-4o", "temperature": 0.7, "max_tokens": 4096};
let overrides = {"temperature": 0.2, "stream": true};
let final_config = defaults.merge(overrides);
emit f"Config: {final_config}";
// {model: gpt-4o, temperature: 0.2, max_tokens: 4096, stream: true}
}
map_values(), map_keys(), filter(), find()
{
let prices = {"apple": 1.20, "banana": 0.50, "cherry": 2.50};
// Transform all values
let doubled = prices.map_values(fn(v) { return v * 2; });
emit f"Doubled: {doubled}"; // {apple: 2.4, banana: 1.0, cherry: 5.0}
// Transform all keys
let upper = prices.map_keys(fn(k) { return upper_case(k); });
emit f"Upper keys: {upper}"; // {APPLE: 1.2, BANANA: 0.5, CHERRY: 2.5}
// Filter entries by a predicate on (key, value)
let expensive = prices.filter(fn(k, v) { return v > 1.0; });
emit f"Expensive: {expensive}"; // {apple: 1.2, cherry: 2.5}
// Find the first entry matching a predicate
let found = prices.find(fn(k, v) { return v < 1.0; });
emit f"Found: {found}"; // (banana, 0.5)
}
for_each(), count(), is_empty(), to_list()
{
let scores = {"Alice": 95, "Bob": 87, "Carol": 92};
// Iterate with a side effect
scores.for_each(fn(k, v) {
emit f"{k} scored {v}";
});
emit f"Entry count: {scores.count()}"; // 3
emit f"Is empty: {scores.is_empty()}"; // false
emit f"As list: {scores.to_list()}"; // [(Alice, 95), (Bob, 87), (Carol, 92)]
}
Complete Map Methods Reference
| Method | Syntax | Returns |
|---|---|---|
get_or |
map.get_or(key, default) |
Value or default |
get_or_insert |
map.get_or_insert(key, default) |
Value (inserts if missing) |
merge |
map.merge(other) |
New combined map |
map_values |
map.map_values(fn) |
New map with transformed values |
map_keys |
map.map_keys(fn) |
New map with transformed keys |
filter |
map.filter(fn(k,v)) |
New map with matching entries |
find |
map.find(fn(k,v)) |
First matching (key, value) or nil |
for_each |
map.for_each(fn(k,v)) |
nil (side effects only) |
count |
map.count() |
Number of entries |
is_empty |
map.is_empty() |
true if map has no entries |
to_list |
map.to_list() |
List of (key, value) tuples |
Sets #
What Is a Set? #
Think of a set like a guest list for a party. You can add names to the list, but writing "Alice" twice does not mean Alice gets two invitations -- she is either on the list or she is not. A set works the same way: every element appears at most once, and the collection does not care about order.
A set is an unordered collection of unique values. Sets are ideal when you need to: - Remove duplicates from data. - Test membership quickly ("Is this item in the collection?"). - Perform mathematical set operations like union, intersection, and difference.
Creating Sets #
There are two ways to create a set -- the set() constructor and the .to_set() method
on lists:
{
// Direct construction with set()
let colors = set("red", "green", "blue");
emit f"Colors: {colors}"; // {red, green, blue}
// Creating from a list (deduplication)
let words = ["hello", "world", "hello", "neam", "world"];
let unique = words.to_set();
emit f"Unique words: {unique}"; // {hello, world, neam}
// Empty set
let empty = set();
emit f"Empty: {empty}"; // {}
}
Output:
Colors: {red, green, blue}
Unique words: {hello, world, neam}
Empty: {}
Notice that the duplicates ("hello" and "world") each appear only once in the set.
Mutating Sets: add() and remove() #
Sets support in-place mutation with add() and remove():
{
let permissions = set("read", "write");
permissions.add("execute");
emit f"After add: {permissions}"; // {read, write, execute}
permissions.add("read"); // no effect -- already present
emit f"After duplicate add: {permissions}"; // {read, write, execute}
permissions.remove("write");
emit f"After remove: {permissions}"; // {read, execute}
}
Set Operations #
Sets really shine when you need to compare two collections. Neam supports the classic set operations: union, intersection, difference, and symmetric difference.
{
let a = set(1, 2, 3, 4);
let b = set(3, 4, 5, 6);
emit f"Union: {a.union(b)}"; // {1, 2, 3, 4, 5, 6}
emit f"Intersection: {a.intersection(b)}"; // {3, 4}
emit f"Difference: {a.difference(b)}"; // {1, 2}
emit f"Sym. Diff: {a.symmetric_diff(b)}"; // {1, 2, 5, 6}
}
Output:
Union: {1, 2, 3, 4, 5, 6}
Intersection: {3, 4}
Difference: {1, 2}
Sym. Diff: {1, 2, 5, 6}
Here is a visual breakdown of how these operations work:
Subset and Superset Tests #
{
let small = set(1, 2);
let big = set(1, 2, 3, 4, 5);
emit f"small subset of big? {small.is_subset(big)}"; // true
emit f"big superset of small? {big.is_superset(small)}"; // true
emit f"big subset of small? {big.is_subset(small)}"; // false
}
Membership Testing and contains() #
One of the most useful features of sets is fast membership testing using the in keyword
or the .contains() method:
{
let a = set(1, 2, 3, 4);
emit f"3 in set: {3 in a}"; // true
emit f"7 in set: {7 in a}"; // false
emit f"contains 2: {a.contains(2)}"; // true
}
Membership testing on a set is much faster than scanning a list, especially for large collections. If your agent needs to check "have I already seen this document ID?" thousands of times, use a set.
Transforming Sets: map(), filter(), to_list() #
Sets support functional transformations that return new collections:
{
let nums = set(1, 2, 3, 4, 5);
// map: apply a function to each element
let doubled = nums.map(fn(n) { return n * 2; });
emit f"Doubled: {doubled}"; // {2, 4, 6, 8, 10}
// filter: keep elements matching a predicate
let evens = nums.filter(fn(n) { return n % 2 == 0; });
emit f"Evens: {evens}"; // {2, 4}
// to_list: convert back to a list (useful for sorting, indexing)
let as_list = nums.to_list();
emit f"As list: {as_list.sort()}"; // [1, 2, 3, 4, 5]
}
An agent receives search results from two different sources. Source A
returns document IDs [101, 102, 103, 104, 105] and Source B returns [103, 105, 106, 107].
Convert both to sets and find: (1) all unique document IDs, (2) documents that appear in
both sources, and (3) documents unique to Source A. Try it in the Neam REPL!
Complete Set Methods Reference
| Method | Syntax | Returns |
|---|---|---|
add |
set.add(val) |
Modifies in place |
remove |
set.remove(val) |
Modifies in place |
contains |
set.contains(val) |
Boolean |
union |
set.union(other) |
New set with all elements |
intersection |
set.intersection(other) |
New set with shared elements |
difference |
set.difference(other) |
New set: in self but not other |
symmetric_diff |
set.symmetric_diff(other) |
New set: in one but not both |
is_subset |
set.is_subset(other) |
Boolean |
is_superset |
set.is_superset(other) |
Boolean |
to_list |
set.to_list() |
Convert to list |
map |
set.map(fn) |
New set with transformed values |
filter |
set.filter(fn) |
New set with matching values |
Tuples #
What Is a Tuple? #
A tuple is a fixed-size, ordered grouping of values. Tuples are like lists, but they are lightweight and immutable -- once created, you cannot change their contents. Tuples are also hashable, which means they can be used as map keys or set elements.
Use tuples when you need to bundle a small number of related values together without the
overhead of creating a map. Common examples: a 2D point (x, y), an RGB color
(r, g, b), or a key-value pair returned from a function.
Creating Tuples #
Tuple literals use parentheses:
{
// Creating tuples
let point = (3.14, 2.72);
emit f"x={point.0}, y={point.1}";
// Mixed types are fine
let result = (true, "ok", 42);
emit f"Status: {result.0}, Message: {result.1}, Code: {result.2}";
// Destructuring tuples
let (x, y) = point;
emit f"Destructured: x={x}, y={y}";
// Tuples as lightweight records
let rgb = (255, 128, 0);
let (r, g, b) = rgb;
emit f"Color: R={r} G={g} B={b}";
}
Output:
x=3.14, y=2.72
Status: true, Message: ok, Code: 42
Destructured: x=3.14, y=2.72
Color: R=255 G=128 B=0
Tuple Methods #
Tuples support a small set of query methods:
{
let data = (10, 20, 30, 40);
emit f"Length: {data.len()}"; // 4
emit f"Has 20: {data.contains(20)}"; // true
emit f"Has 99: {data.contains(99)}"; // false
emit f"As list: {data.to_list()}"; // [10, 20, 30, 40]
}
Tuples as Map Keys
Because tuples are immutable and hashable, they make excellent map keys for coordinate lookups and multi-part keys:
{
let distances = {
("London", "Paris"): 340,
("Paris", "Berlin"): 878,
("London", "Berlin"): 930
};
emit f"London to Paris: {distances[("London", "Paris")]} km";
}
When to Use Tuples vs Maps vs Lists #
Each collection type has a sweet spot. Choosing the right one makes your code clearer and more efficient:
Rules of thumb:
- If the number of items can change, use a list.
- If you need to look up values by name, use a map.
- If you need uniqueness or set math, use a set.
- If you have 2-5 related values and creating a map feels like overkill, use a tuple.
Complete Tuple Methods Reference
| Method | Syntax | Returns |
|---|---|---|
len |
tuple.len() |
Number of elements |
contains |
tuple.contains(val) |
true if val is in tuple |
to_list |
tuple.to_list() |
Mutable list copy |
| Access | tuple.0, tuple.1 |
Element at position |
| Destructure | let (x, y) = tuple; |
Binds elements to variables |
Range #
A range represents a lazy numeric sequence. Ranges do not allocate a list of values in memory -- they compute elements on demand, making them ideal for counted iteration and membership testing.
Creating Ranges #
{
// range(end) -- 0 to end-1
for (i in range(5)) {
emit f"i = {i}";
}
// Output: i = 0, i = 1, i = 2, i = 3, i = 4
// range(start, end) -- start to end-1
for (i in range(3, 7)) {
emit f"i = {i}";
}
// Output: i = 3, i = 4, i = 5, i = 6
// range(start, end, step)
for (i in range(0, 20, 5)) {
emit f"i = {i}";
}
// Output: i = 0, i = 5, i = 10, i = 15
}
Range Methods #
Ranges support O(1) membership testing -- checking whether a number falls within the range does not require iterating through it:
{
let r = range(0, 100, 2); // even numbers 0-98
emit f"Length: {r.len()}"; // 50
emit f"Has 42: {r.contains(42)}"; // true (O(1) -- no iteration!)
emit f"Has 43: {r.contains(43)}"; // false (43 is odd)
// Convert to list (materializes the full sequence)
let evens = range(0, 10, 2).to_list();
emit f"Evens: {evens}"; // [0, 2, 4, 6, 8]
// Reverse
emit f"Reversed: {range(1, 6).reverse().to_list()}"; // [5, 4, 3, 2, 1]
}
Functional Operations on Ranges #
Ranges support map(), filter(), and fold() without materializing the full sequence:
{
// Squares of 1 to 10
let squares = range(1, 11).map(fn(n) { return n * n; });
emit f"Squares: {squares}"; // [1, 4, 9, 16, 25, 36, 49, 64, 81, 100]
// Sum of 1 to 100
let total = range(1, 101).fold(0, fn(acc, n) { return acc + n; });
emit f"Sum 1-100: {total}"; // 5050
// Odd numbers in a range
let odds = range(1, 20).filter(fn(n) { return n % 2 != 0; });
emit f"Odds: {odds}"; // [1, 3, 5, 7, 9, 11, 13, 15, 17, 19]
}
Complete Range Methods Reference
| Method | Syntax | Returns |
|---|---|---|
len |
range.len() |
Number of elements |
contains |
range.contains(val) |
O(1) membership test |
to_list |
range.to_list() |
Materialized list |
reverse |
range.reverse() |
Reversed range |
map |
range.map(fn) |
Transformed list |
filter |
range.filter(fn) |
Filtered list |
fold |
range.fold(init, fn) |
Single accumulated value |
TypedArray #
A TypedArray is a dense, contiguous array of numbers (floats or ints) that supports vectorized math. While a regular Neam list can hold any mix of types, a TypedArray restricts its elements to a single numeric type, enabling fast bulk operations that are critical for AI workloads like embeddings, scoring, and numeric analysis.
Creating TypedArrays #
{
// Fixed-size float array (initialized to zeros)
let embedding = float_array(768);
emit f"Embedding length: {embedding.len()}"; // 768
// From a literal list of floats
let scores = float_array([0.95, 0.87, 0.62, 0.91]);
emit f"Scores: {scores}";
// Integer typed array
let ids = int_array([101, 202, 303, 404]);
emit f"IDs: {ids}";
}
Vectorized Operations #
TypedArrays support element-wise arithmetic and vector math without explicit loops:
{
let a = float_array([1.0, 2.0, 3.0, 4.0]);
let b = float_array([0.5, 1.5, 2.5, 3.5]);
// Element-wise operations
emit f"a + b = {a + b}"; // [1.5, 3.5, 5.5, 7.5]
emit f"a * b = {a * b}"; // [0.5, 3.0, 7.5, 14.0]
emit f"a * 2 = {a * 2.0}"; // [2.0, 4.0, 6.0, 8.0]
// Dot product (sum of element-wise products)
emit f"a . b = {a.dot(b)}"; // 28.0
// Norm (Euclidean length)
emit f"||a|| = {a.norm()}"; // 5.477...
}
Statistical and Aggregation Methods #
{
let data = float_array([3.0, 1.0, 4.0, 1.0, 5.0, 9.0, 2.0, 6.0]);
emit f"Sum: {data.sum()}"; // 31.0
emit f"Mean: {data.mean()}"; // 3.875
emit f"Std: {data.std()}"; // 2.588...
emit f"Min: {data.min()}"; // 1.0
emit f"Max: {data.max()}"; // 9.0
emit f"Argmin: {data.argmin()}"; // 1 (index of minimum)
emit f"Argmax: {data.argmax()}"; // 5 (index of maximum)
emit f"Cumsum: {data.cumsum()}"; // [3, 4, 8, 9, 14, 23, 25, 31]
emit f"Abs: float_array([-1.0, 2.0, -3.0]).abs()"; // [1.0, 2.0, 3.0]
}
Sorting and Slicing #
{
let data = float_array([3.0, 1.0, 4.0, 1.0, 5.0]);
emit f"Sorted: {data.sort()}"; // [1.0, 1.0, 3.0, 4.0, 5.0]
emit f"Slice: {data.slice(1, 4)}"; // [1.0, 4.0, 1.0]
}
Agent Use Case: Cosine Similarity #
Embedding similarity is one of the most common operations in AI agents:
fun cosine_similarity(a, b) {
return a.dot(b) / (a.norm() * b.norm());
}
{
let query_embed = float_array([0.9, 0.1, 0.3, 0.7]);
let doc_embed = float_array([0.8, 0.2, 0.4, 0.6]);
let sim = cosine_similarity(query_embed, doc_embed);
emit f"Similarity: {sim}"; // 0.985...
}
Complete TypedArray Methods Reference
| Method | Syntax | Returns |
|---|---|---|
sum |
arr.sum() |
Sum of all elements |
mean |
arr.mean() |
Average of elements |
std |
arr.std() |
Standard deviation |
min |
arr.min() |
Minimum value |
max |
arr.max() |
Maximum value |
norm |
arr.norm() |
Euclidean norm |
dot |
arr.dot(other) |
Dot product |
sort |
arr.sort() |
New sorted array |
slice |
arr.slice(start, end) |
Sub-array |
argmin |
arr.argmin() |
Index of minimum |
argmax |
arr.argmax() |
Index of maximum |
cumsum |
arr.cumsum() |
Cumulative sum array |
abs |
arr.abs() |
Absolute values |
Record #
A record is a named, immutable data structure with typed fields. Records are like maps but with a fixed shape defined at compile time. They provide the clarity of named fields with the safety of immutability.
Defining and Creating Records #
// Define a record type
record Point {
x: number,
y: number
}
record AgentConfig {
name: string,
model: string,
temperature: number
}
{
// Create record instances (positional arguments)
let origin = Point(0, 0);
let p = Point(10, 20);
emit f"Point: ({p.x}, {p.y})"; // Point: (10, 20)
// Named fields
let config = AgentConfig("researcher", "gpt-4o", 0.7);
emit f"Agent: {config.name}, model: {config.model}";
}
Record Immutability and with() #
Records are immutable -- you cannot change a field after creation. Instead, use with()
to create a new record with one or more fields changed:
record Config {
model: string,
temperature: number
}
{
let base = Config("gpt-4o", 0.7);
// with() creates a new record with updated fields
let creative = base.with({"temperature": 0.95});
let fast = base.with({"model": "gpt-4o-mini", "temperature": 0.3});
emit f"Base: model={base.model}, temp={base.temperature}";
emit f"Creative: model={creative.model}, temp={creative.temperature}";
emit f"Fast: model={fast.model}, temp={fast.temperature}";
}
Output:
Base: model=gpt-4o, temp=0.7
Creative: model=gpt-4o, temp=0.95
Fast: model=gpt-4o-mini, temp=0.3
Record Methods #
record Color { r: number, g: number, b: number }
{
let red = Color(255, 0, 0);
// Convert to map
let as_map = red.to_map();
emit f"As map: {as_map}"; // {r: 255, g: 0, b: 0}
// Get field names
emit f"Fields: {red.fields()}"; // [r, g, b]
}
Complete Record Methods Reference
| Method | Syntax | Returns |
|---|---|---|
to_map |
record.to_map() |
Map of field names to values |
with |
record.with(changes_map) |
New record with updated fields |
fields |
record.fields() |
List of field name strings |
Table #
A Table is a columnar data structure for analytics and data processing. Tables are Neam's answer to data frames -- they let agents filter, sort, group, aggregate, and transform structured data without external libraries.
Creating Tables #
{
// From a list of maps (each map is a row)
let sales = table([
{"product": "Widget", "region": "North", "revenue": 1200},
{"product": "Gadget", "region": "South", "revenue": 800},
{"product": "Widget", "region": "South", "revenue": 950},
{"product": "Gadget", "region": "North", "revenue": 1100},
{"product": "Widget", "region": "North", "revenue": 1350}
]);
emit sales.to_string();
}
Output:
Accessing Rows and Columns #
{
let t = table([
{"name": "Alice", "score": 95},
{"name": "Bob", "score": 87},
{"name": "Carol", "score": 92}
]);
// Access a column (returns a list)
emit f"Names: {t.col("name")}"; // [Alice, Bob, Carol]
emit f"Scores: {t.col("score")}"; // [95, 87, 92]
// Access a row (returns a map)
emit f"Row 0: {t.row(0)}"; // {name: Alice, score: 95}
// Preview
emit t.head(2).to_string(); // first 2 rows
emit t.tail(1).to_string(); // last 1 row
}
Filtering and Sorting #
{
let students = table([
{"name": "Alice", "grade": 95, "dept": "CS"},
{"name": "Bob", "grade": 72, "dept": "Math"},
{"name": "Carol", "grade": 88, "dept": "CS"},
{"name": "Dave", "grade": 91, "dept": "Math"},
{"name": "Eve", "grade": 67, "dept": "CS"}
]);
// Filter: only CS students with grade > 80
let top_cs = students
.filter(fn(row) { return row.dept == "CS" && row.grade > 80; });
emit top_cs.to_string();
// Sort by grade descending
let ranked = students.sort_by("grade", "desc");
emit ranked.to_string();
// Select specific columns
let names_only = students.select(["name", "grade"]);
emit names_only.to_string();
}
Adding Columns #
{
let t = table([
{"name": "Alice", "raw_score": 85},
{"name": "Bob", "raw_score": 92},
{"name": "Carol", "raw_score": 78}
]);
// Add a computed column
let with_pct = t.add_column("percent", fn(row) {
return row.raw_score / 100.0;
});
emit with_pct.to_string();
}
group_by() + agg() -- Aggregation #
This is one of the most powerful Table operations, mirroring SQL's GROUP BY:
{
let sales = table([
{"product": "Widget", "region": "North", "revenue": 1200},
{"product": "Gadget", "region": "South", "revenue": 800},
{"product": "Widget", "region": "South", "revenue": 950},
{"product": "Gadget", "region": "North", "revenue": 1100},
{"product": "Widget", "region": "North", "revenue": 1350}
]);
// Group by product, aggregate revenue
let summary = sales
.group_by("product")
.agg({
"total_revenue": ("revenue", "sum"),
"avg_revenue": ("revenue", "mean"),
"count": ("revenue", "count")
});
emit summary.to_string();
}
Output:
join() -- Combining Tables #
{
let orders = table([
{"order_id": 1, "customer": "Alice", "product_id": 101},
{"order_id": 2, "customer": "Bob", "product_id": 102},
{"order_id": 3, "customer": "Alice", "product_id": 101}
]);
let products = table([
{"product_id": 101, "name": "Widget", "price": 9.99},
{"product_id": 102, "name": "Gadget", "price": 24.50}
]);
let joined = orders.join(products, "product_id");
emit joined.to_string();
}
pivot() -- Reshape Data #
{
let data = table([
{"month": "Jan", "category": "A", "sales": 100},
{"month": "Jan", "category": "B", "sales": 200},
{"month": "Feb", "category": "A", "sales": 150},
{"month": "Feb", "category": "B", "sales": 180}
]);
let pivoted = data.pivot("month", "category", "sales");
emit pivoted.to_string();
}
Output:
Export: to_csv(), to_json() #
{
let t = table([
{"name": "Alice", "score": 95},
{"name": "Bob", "score": 87}
]);
// Export as CSV string
let csv = t.to_csv();
emit csv;
// name,score
// Alice,95
// Bob,87
// Export as JSON string
let json = t.to_json();
emit json;
}
Complete Table Methods Reference
| Method | Syntax | Returns |
|---|---|---|
col |
table.col(name) |
List of column values |
row |
table.row(index) |
Map of one row |
filter |
table.filter(fn(row)) |
New filtered table |
sort_by |
table.sort_by(col, dir) |
New sorted table |
select |
table.select(cols) |
Table with selected columns |
add_column |
table.add_column(name, fn) |
Table with new column |
group_by |
table.group_by(col) |
Grouped table (chain .agg()) |
agg |
grouped.agg(spec) |
Aggregated table |
join |
table.join(other, on) |
Joined table |
pivot |
table.pivot(row, col, val) |
Pivoted table |
head |
table.head(n) |
First n rows |
tail |
table.tail(n) |
Last n rows |
to_csv |
table.to_csv() |
CSV string |
to_json |
table.to_json() |
JSON string |
to_string |
table.to_string() |
Formatted table string |
Iterator Protocol #
Every collection in Neam supports the iterator protocol -- a standardized way to produce elements lazily, one at a time. Iterators let you build efficient data pipelines where each element flows through the entire chain of operations before the next element is produced. No intermediate lists are allocated.
Creating Iterators #
Call .iter() on any collection to get a lazy iterator:
{
let numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
// Eager (allocates intermediate lists):
let eager = numbers
.filter(fn(n) { return n % 2 == 0; }) // [2, 4, 6, 8, 10] <-- list
.map(fn(n) { return n * n; }); // [4, 16, 36, 64, 100] <-- list
// Lazy (no intermediate lists):
let lazy = numbers.iter()
.filter(fn(n) { return n % 2 == 0; }) // iterator adapter
.map(fn(n) { return n * n; }) // iterator adapter
.to_list(); // materialized only here
emit f"Eager: {eager}";
emit f"Lazy: {lazy}";
// Both produce [4, 16, 36, 64, 100]
}
Eager vs Lazy Evaluation
Eager: [1..10] --> filter --> [2,4,6,8,10] --> map --> [4,16,36,64,100]
^ ^
allocates list allocates list
Lazy: [1..10].iter() --> filter --> map --> to_list()
| | |
no alloc no alloc one final list
Each element flows: 1(skip) 2-->4 3(skip) 4-->16 5(skip) ...
Iterator Adapters #
Adapters transform an iterator into another iterator without consuming it. They are lazy -- they do no work until a terminal operation pulls elements through:
{
let data = [5, 3, 8, 1, 9, 2, 7, 4, 6];
// Chain multiple adapters
let result = data.iter()
.filter(fn(n) { return n > 3; }) // keep > 3
.map(fn(n) { return n * 10; }) // multiply by 10
.take(4) // only first 4
.to_list();
emit f"Result: {result}"; // [50, 80, 90, 70]
}
Available Adapters:
{
let items = [1, 2, 3, 4, 5, 6, 7, 8];
// take / skip
emit items.iter().take(3).to_list(); // [1, 2, 3]
emit items.iter().skip(5).to_list(); // [6, 7, 8]
// take_while / skip_while
emit items.iter().take_while(fn(n) { return n < 5; }).to_list(); // [1, 2, 3, 4]
emit items.iter().skip_while(fn(n) { return n < 5; }).to_list(); // [5, 6, 7, 8]
// chain: concatenate two iterators
let a = [1, 2, 3];
let b = [4, 5, 6];
emit a.iter().chain(b.iter()).to_list(); // [1, 2, 3, 4, 5, 6]
// zip: pair up two iterators
let names = ["Alice", "Bob"];
let scores = [95, 87];
emit names.iter().zip(scores.iter()).to_list(); // [(Alice, 95), (Bob, 87)]
// enumerate: pair each element with its index
emit ["a", "b", "c"].iter().enumerate().to_list(); // [(0, a), (1, b), (2, c)]
// flat_map: map then flatten
emit [[1,2],[3,4]].iter().flat_map(fn(x) { return x.iter(); }).to_list();
// [1, 2, 3, 4]
// chunk and window
emit [1,2,3,4,5].iter().chunk(2).to_list(); // [[1,2], [3,4], [5]]
emit [1,2,3,4,5].iter().window(3).to_list(); // [[1,2,3], [2,3,4], [3,4,5]]
// unique: deduplicate
emit [1,2,2,3,3,3].iter().unique().to_list(); // [1, 2, 3]
// flatten: flatten one level of nesting
emit [[1,2],[3],[4,5]].iter().flatten().to_list(); // [1, 2, 3, 4, 5]
// inspect: peek at elements (for debugging, does not change the stream)
[1,2,3].iter()
.inspect(fn(n) { emit f" saw {n}"; })
.map(fn(n) { return n * 2; })
.to_list();
}
Terminal Operations #
Terminals consume the iterator and produce a final value:
{
let nums = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
// to_list, to_set, to_map
let list = nums.iter().filter(fn(n) { return n > 5; }).to_list(); // [6,7,8,9,10]
let uniq = nums.iter().to_set(); // {1..10}
// collect (generic -- type inferred from context)
let collected = nums.iter().filter(fn(n) { return n % 2 == 0; }).collect();
// fold: reduce to a single value
let sum = nums.iter().fold(0, fn(acc, n) { return acc + n; });
emit f"Sum: {sum}"; // 55
// for_each: side effects
nums.iter().take(3).for_each(fn(n) { emit f" n={n}"; });
// Aggregation terminals
emit f"Count: {nums.iter().count()}"; // 10
emit f"Sum: {nums.iter().sum()}"; // 55
emit f"Min: {nums.iter().min()}"; // 1
emit f"Max: {nums.iter().max()}"; // 10
// Predicate terminals
emit f"Any > 5: {nums.iter().any(fn(n) { return n > 5; })}"; // true
emit f"All > 0: {nums.iter().all(fn(n) { return n > 0; })}"; // true
// find: first matching element
let found = nums.iter().find(fn(n) { return n > 7; });
emit f"First > 7: {found}"; // 8
}
Complete Iterator Reference
Adapters (lazy, return iterator):
| Adapter | Syntax | Description |
|---|---|---|
map |
.map(fn) |
Transform each element |
filter |
.filter(fn) |
Keep matching elements |
take |
.take(n) |
First n elements |
skip |
.skip(n) |
Skip first n elements |
chain |
.chain(other_iter) |
Concatenate iterators |
zip |
.zip(other_iter) |
Pair elements from two iterators |
enumerate |
.enumerate() |
Pair each element with its index |
flat_map |
.flat_map(fn) |
Map then flatten |
take_while |
.take_while(fn) |
Take while predicate is true |
skip_while |
.skip_while(fn) |
Skip while predicate is true |
chunk |
.chunk(n) |
Group into chunks of n |
window |
.window(n) |
Sliding windows of size n |
unique |
.unique() |
Deduplicate |
flatten |
.flatten() |
Flatten one level |
inspect |
.inspect(fn) |
Peek at elements (for debugging) |
Terminals (consume iterator, return value):
| Terminal | Syntax | Returns |
|---|---|---|
to_list |
.to_list() |
List |
to_set |
.to_set() |
Set |
to_map |
.to_map() |
Map (from key-value pairs) |
fold |
.fold(init, fn) |
Single accumulated value |
for_each |
.for_each(fn) |
nil (side effects) |
count |
.count() |
Number of elements |
sum |
.sum() |
Sum |
min |
.min() |
Minimum |
max |
.max() |
Maximum |
any |
.any(fn) |
true if any match |
all |
.all(fn) |
true if all match |
find |
.find(fn) |
First match or nil |
collect |
.collect() |
Collected result (type inferred) |
Broadcasting #
Broadcasting applies arithmetic operations element-wise across lists and typed arrays. When one operand is a scalar (a single number), it is "broadcast" to match the size of the collection. When both operands are collections of the same length, operations are applied pairwise.
List Broadcasting #
{
let prices = [10, 20, 30, 40, 50];
// Scalar broadcast: add 10 to every element
emit prices + 10; // [20, 30, 40, 50, 60]
// Scalar broadcast: multiply by 1.1 (10% markup)
emit prices * 1.1; // [11.0, 22.0, 33.0, 44.0, 55.0]
// Pairwise: element-wise multiplication
let quantities = [5, 3, 8, 2, 7];
emit prices * quantities; // [50, 60, 240, 80, 350]
// Pairwise: element-wise addition
let discounts = [1, 2, 3, 4, 5];
emit prices - discounts; // [9, 18, 27, 36, 45]
}
Broadcasting Visualized
Scalar: [10, 20, 30] + 5
[10, 20, 30]
+ [ 5, 5, 5] <-- scalar expanded
= [15, 25, 35]
Pairwise: [1, 2, 3] * [4, 5, 6]
[1, 2, 3]
* [4, 5, 6]
= [4, 10, 18]
TypedArray Broadcasting #
TypedArrays support the same broadcasting, but operations run on contiguous numeric storage for maximum performance:
{
let embeddings = float_array([0.9, 0.1, 0.3, 0.7]);
// Normalize by dividing by norm
let normalized = embeddings / embeddings.norm();
emit f"Normalized: {normalized}";
// Scale all values
let scaled = embeddings * 100.0;
emit f"Scaled: {scaled}"; // [90.0, 10.0, 30.0, 70.0]
}
Spread Operator #
The spread operator (...) unpacks the elements of a collection into a new collection
literal. It works on lists, maps, and sets, and is the idiomatic way to merge, clone, or
extend collections.
Spreading Lists #
{
let head = [1, 2, 3];
let tail = [7, 8, 9];
// Merge lists
let combined = [...head, 4, 5, 6, ...tail];
emit f"Combined: {combined}"; // [1, 2, 3, 4, 5, 6, 7, 8, 9]
// Clone a list
let clone = [...head];
emit f"Clone: {clone}"; // [1, 2, 3]
// Prepend / append
let with_zero = [0, ...head];
emit f"With zero: {with_zero}"; // [0, 1, 2, 3]
}
Spreading Maps #
{
let defaults = {"model": "gpt-4o", "temperature": 0.7, "max_tokens": 4096};
let overrides = {"temperature": 0.2, "stream": true};
// Merge maps (later spread wins on key conflicts)
let config = {...defaults, ...overrides};
emit f"Config: {config}";
// {model: gpt-4o, temperature: 0.2, max_tokens: 4096, stream: true}
// Add a single field
let extended = {...defaults, "top_p": 0.9};
emit f"Extended: {extended}";
}
Agent Use Case: Config Layering #
The spread operator is perfect for agent configuration layering, where you have a base config overridden by environment-specific and user-specific settings:
{
let base_config = {
"model": "gpt-4o",
"temperature": 0.7,
"max_tokens": 4096,
"stream": false
};
let env_config = {
"max_tokens": 8192,
"stream": true
};
let user_config = {
"temperature": 0.2
};
// Layer: base < env < user (later wins)
let final_config = {...base_config, ...env_config, ...user_config};
emit f"Final: {final_config}";
// {model: gpt-4o, temperature: 0.2, max_tokens: 8192, stream: true}
}
Nested Collections #
Lists and maps can be nested to arbitrary depth. This is essential for representing structured data -- agent configurations, API responses, knowledge base results, and more.
List of Maps #
{
let team = [
{"name": "Alice", "role": "Engineer"},
{"name": "Bob", "role": "Designer"},
{"name": "Carol", "role": "Manager"}
];
for (member in team) {
emit member.name + " -- " + member.role;
}
}
Output:
Alice -- Engineer
Bob -- Designer
Carol -- Manager
Map of Lists #
{
let courses = {
"math": ["Calculus", "Linear Algebra", "Statistics"],
"cs": ["Algorithms", "Operating Systems", "AI"]
};
emit "Math courses:";
for (course in courses["math"]) {
emit " - " + course;
}
emit "CS courses:";
for (course in courses["cs"]) {
emit " - " + course;
}
}
Output:
Math courses:
- Calculus
- Linear Algebra
- Statistics
CS courses:
- Algorithms
- Operating Systems
- AI
Deeply Nested Structures #
{
let org = {
"company": "Acme AI",
"departments": [
{
"name": "Engineering",
"teams": [
{"name": "Platform", "headcount": 12},
{"name": "ML", "headcount": 8}
]
},
{
"name": "Product",
"teams": [
{"name": "Design", "headcount": 5}
]
}
]
};
// Access deeply nested data
let first_dept = org["departments"][0];
let first_team = first_dept["teams"][0];
emit org["company"] + " > " + first_dept["name"] + " > " + first_team["name"];
emit "Headcount: " + str(first_team["headcount"]);
}
Output:
Acme AI > Engineering > Platform
Headcount: 12
Memory Layout #
The following diagram illustrates how lists and maps are represented internally. Lists store contiguous indexed slots. Maps store key-value pairs in a hash table. Both hold references to their elements, enabling nesting without deep copying.
Lists grow dynamically when you call push(). The underlying array is resized as needed.
Maps use hash-based lookup, providing average constant-time access by key.
Practical Patterns #
The following four patterns appear repeatedly in agent systems. Master them and you will be able to handle most data-processing tasks in Neam.
Pattern 1: Accumulation #
Build up a result by iterating over a list and collecting values:
// Sum all numbers in a list
fun sum_list(numbers) {
let total = 0;
let i = 0;
while (i < len(numbers)) {
total = total + numbers[i];
i = i + 1;
}
return total;
}
{
let scores = [92, 85, 78, 96, 88];
emit "Total: " + str(sum_list(scores));
emit "Average: " + str(sum_list(scores) / len(scores));
}
Output:
Total: 439
Average: 87.8
A string accumulation variant concatenates results:
fun join_list(items, separator) {
let result = "";
let i = 0;
while (i < len(items)) {
if (i > 0) {
result = result + separator;
}
result = result + items[i];
i = i + 1;
}
return result;
}
{
let tags = ["ai", "agents", "neam", "rag"];
emit "Tags: " + join_list(tags, ", ");
}
Output:
Tags: ai, agents, neam, rag
Pattern 2: Filtering #
Create a new list containing only elements that satisfy a condition:
fun filter_passing(scores, threshold) {
let passing = [];
let i = 0;
while (i < len(scores)) {
if (scores[i] >= threshold) {
push(passing, scores[i]);
}
i = i + 1;
}
return passing;
}
{
let all_scores = [45, 82, 67, 91, 55, 78, 33, 96];
let passing = filter_passing(all_scores, 70);
emit "Passing scores: " + str(passing);
emit "Count: " + str(len(passing)) + " out of " + str(len(all_scores));
}
Output:
Passing scores: [82, 91, 78, 96]
Count: 4 out of 8
Pattern 3: Transformation (Map) #
Build a new list by applying a function to each element of an existing list:
fun double_all(numbers) {
let result = [];
let i = 0;
while (i < len(numbers)) {
push(result, numbers[i] * 2);
i = i + 1;
}
return result;
}
{
let original = [1, 2, 3, 4, 5];
let doubled = double_all(original);
emit "Original: " + str(original);
emit "Doubled: " + str(doubled);
}
Output:
Original: [1, 2, 3, 4, 5]
Doubled: [2, 4, 6, 8, 10]
Pattern 4: Lookup Table #
Use a map as a lookup table for fast key-based retrieval:
{
let status_messages = {
"200": "OK",
"404": "Not Found",
"500": "Internal Server Error",
"429": "Rate Limited"
};
let codes = ["200", "404", "429", "500"];
let i = 0;
while (i < len(codes)) {
let code = codes[i];
let message = status_messages[code];
emit "HTTP " + code + ": " + message;
i = i + 1;
}
}
Output:
HTTP 200: OK
HTTP 404: Not Found
HTTP 429: Rate Limited
HTTP 500: Internal Server Error
Functional Operations on Collections #
In Chapter 5, you learned about anonymous functions (fn) and higher-order functions like
map(), filter(), and fold(). These functions are particularly powerful when combined
with collections, allowing you to express complex data transformations concisely.
map() -- Transform Every Element #
The map() function applies a function to each element of a list and returns a new list
of the results:
{
let prices = [10.0, 25.5, 8.99, 42.0];
// Apply 20% discount to all prices
let discounted = map(prices, fn(price) {
return price * 0.8;
});
emit "Original: " + str(prices);
emit "Discounted: " + str(discounted);
}
Output:
Original: [10, 25.5, 8.99, 42]
Discounted: [8, 20.4, 7.192, 33.6]
filter() -- Select Matching Elements #
The filter() function returns a new list containing only elements for which the
predicate function returns true:
{
let scores = [45, 82, 67, 91, 55, 78, 33, 96];
let passing = filter(scores, fn(score) {
return score >= 70;
});
emit "All scores: " + str(scores);
emit "Passing: " + str(passing);
}
Output:
All scores: [45, 82, 67, 91, 55, 78, 33, 96]
Passing: [82, 91, 78, 96]
fold() -- Reduce to a Single Value #
The fold() function accumulates a single result by applying a function to each element
along with a running accumulator:
{
let numbers = [1, 2, 3, 4, 5];
let sum = fold(numbers, 0, fn(acc, n) {
return acc + n;
});
let product = fold(numbers, 1, fn(acc, n) {
return acc * n;
});
emit "Sum: " + str(sum);
emit "Product: " + str(product);
}
Output:
Sum: 15
Product: 120
Chaining Functional Operations #
These operations can be chained to build expressive data pipelines:
{
let products = [
{"name": "Widget A", "price": 9.99, "in_stock": true},
{"name": "Widget B", "price": 24.50, "in_stock": false},
{"name": "Widget C", "price": 5.00, "in_stock": true},
{"name": "Widget D", "price": 49.99, "in_stock": true}
];
// Get names of in-stock products under $20
let affordable = filter(products, fn(p) {
return p.in_stock && p.price < 20;
});
let names = map(affordable, fn(p) {
return p.name;
});
emit "Affordable in-stock: " + str(names);
// Calculate total value of in-stock items
let in_stock = filter(products, fn(p) { return p.in_stock; });
let total = fold(in_stock, 0, fn(acc, p) { return acc + p.price; });
emit "Total in-stock value: $" + str(total);
}
Output:
Affordable in-stock: [Widget A, Widget C]
Total in-stock value: $64.98
Functional operations return new lists -- the originals are never modified. This makes your code safer and easier to reason about, especially in agent systems where multiple operations may share the same data.
JSON and Collections #
In agent systems, data frequently arrives as JSON from APIs, LLM responses, and configuration files. Neam maps and lists map directly to JSON objects and arrays, making the conversion seamless.
Parsing JSON Strings #
The built-in json_parse() function converts a JSON string into a Neam map or list:
{
let json_str = '{"name": "Alice", "scores": [95, 87, 92]}';
let data = json_parse(json_str);
emit "Name: " + data.name;
emit "First score: " + str(data.scores[0]);
emit "All scores: " + str(data.scores);
}
Output:
Name: Alice
First score: 95
All scores: [95, 87, 92]
Converting Collections to JSON #
The built-in json_stringify() function converts a Neam map or list into a JSON string:
{
let agent_config = {
"provider": "openai",
"model": "gpt-4o",
"parameters": {
"temperature": 0.7,
"max_tokens": 4096
}
};
let json_output = json_stringify(agent_config);
emit "JSON: " + json_output;
}
Practical Pattern: Processing API Responses #
A common agent pattern is receiving JSON, extracting relevant data, and transforming it:
fun process_api_response(json_response) {
let data = json_parse(json_response);
if (!data.ok) {
return {"ok": false, "error": "API returned error: " + data.error};
}
// Extract and transform results
let results = map(data.results, fn(item) {
return {
"title": item.title,
"relevance": item.score
};
});
// Filter for high-relevance results
let top_results = filter(results, fn(r) {
return r.relevance > 0.7;
});
return {"ok": true, "value": top_results};
}
Putting It Together: Agent Data Processing #
The following example combines loops, functional operations, and collections in a realistic agent scenario. An agent collects search results, filters for relevance, transforms them into a summary format, and accumulates a final report:
// Simulated search results from a RAG pipeline
fun get_search_results() {
return [
{"title": "Neam Language Guide", "score": 0.95, "snippet": "Neam is a language for AI agents..."},
{"title": "Python AI Frameworks", "score": 0.42, "snippet": "Python has many AI libraries..."},
{"title": "Agent Orchestration", "score": 0.88, "snippet": "Multi-agent systems coordinate..."},
{"title": "Web Development 101", "score": 0.15, "snippet": "HTML and CSS basics..."},
{"title": "RAG Strategies", "score": 0.91, "snippet": "Retrieval-augmented generation..."}
];
}
{
let results = get_search_results();
emit "Total results: " + str(len(results));
// Filter: keep only results above a relevance threshold
let relevant = filter(results, fn(r) { return r.score >= 0.7; });
emit "Relevant results: " + str(len(relevant));
// Transform: extract just titles and scores into a summary format
let summaries = map(relevant, fn(r) {
return {"title": r.title, "relevance": str(r.score * 100) + "%"};
});
// Accumulate: build a text report
let report = "=== Relevant Results ===\n";
for (i, s in enumerate(summaries)) {
report = report + str(i + 1) + ". " + s.title + " (" + s.relevance + ")\n";
}
emit report;
}
Expected output:
Total results: 5
Relevant results: 3
=== Relevant Results ===
1. Neam Language Guide (95%)
2. Agent Orchestration (88%)
3. RAG Strategies (91%)
Collections in Agent Development #
Collections are at the heart of every agent system. Here are the most common ways you will use lists and maps when building agents.
Conversation History #
Agents maintain conversation history as a list of maps:
{
let history = [];
// Each message is a map with role and content
push(history, {"role": "user", "content": "What is Neam?"});
push(history, {"role": "assistant", "content": "Neam is a language for AI agents."});
push(history, {"role": "user", "content": "How do I create one?"});
// Count messages by role
let user_count = len(filter(history, fn(m) { return m.role == "user"; }));
let assistant_count = len(filter(history, fn(m) { return m.role == "assistant"; }));
emit "User messages: " + str(user_count);
emit "Assistant messages: " + str(assistant_count);
}
Skill Registry #
Maps make excellent registries for looking up skills by name:
{
let skills = {
"search": {"description": "Search the knowledge base", "required_params": ["query"]},
"calculate": {"description": "Perform arithmetic", "required_params": ["expression"]},
"summarize": {"description": "Summarize text", "required_params": ["text", "max_length"]}
};
let skill_name = "search";
if (contains(skills, skill_name)) {
let skill = skills[skill_name];
emit "Skill: " + skill_name;
emit "Description: " + skill.description;
emit "Required params: " + str(skill.required_params);
}
}
Processing LLM Results #
Agent pipelines frequently collect, rank, and merge results from multiple sources:
fun merge_and_rank(source1_results, source2_results) {
// Combine results from multiple sources
let all_results = concat(source1_results, source2_results);
// Filter for minimum quality
let quality = filter(all_results, fn(r) { return r.score > 0.5; });
// Sort by score (descending -- reverse after sort)
let ranked = reverse(sort(map(quality, fn(r) {
return {"score": r.score, "title": r.title, "source": r.source};
})));
return ranked;
}
Common Mistakes and How to Avoid Them #
Off-by-one errors. The most common mistake with lists is using <= instead of <
in the loop condition. Since lists are zero-indexed, the last valid index is
len(list) - 1. Use while (i < len(list)), not while (i <= len(list)).
Modifying a list during iteration. If you push() to a list while iterating over it
with an index-based loop, the length changes mid-iteration. This can cause skipped
elements or infinite loops. Build a new list instead.
Assuming map key order. Maps do not guarantee insertion order. If you need ordered keys, maintain a separate list of keys.
Forgetting str() for non-string values. When concatenating numbers or booleans with
strings using +, wrap the non-string value in str():
{
let count = 42;
// This works:
emit "Count: " + str(count);
// This would cause a type error:
// emit "Count: " + count;
}
Summary #
This chapter covered all ten collection-related constructs in Neam:
| Type | Create | Access | Mutable? |
|---|---|---|---|
| List | [a, b, c] |
list[i], list[1:3] |
Yes |
| Map | {"k": v}, {1: v} |
map["k"], map.k |
Yes |
| Set | set(a, b), list.to_set() |
val in set, .contains() |
Yes (add/remove) |
| Tuple | (a, b, c) |
tuple.0, tuple.1 |
No (immutable) |
| Range | range(10), range(1, 11, 2) |
for (i in range) |
No (immutable) |
| TypedArray | float_array(n), int_array([...]) |
arr[i] |
Yes |
| Record | RecordName(args) |
rec.field |
No (use .with()) |
| Table | table([...]) |
.col(), .row() |
No (returns new tables) |
| Iterator | coll.iter() |
Chain adapters, then terminal | Consumed once |
| Broadcasting | list + 10, list * list |
Element-wise result | Returns new collection |
You learned:
- Lists -- creation, indexing, slicing with negative indices, 25+ methods including
slice(),flatten(),flat_map(),zip(),enumerate(),take(),skip(),any(),all(),count(),group_by(),unique(),chunk(),window(),sort_by(),min(),max(),sum(),mean(),join(),to_set(),contains(),first(),last(), andis_empty(). - Maps -- string and non-string keys (numbers, tuples), methods including
get_or(),get_or_insert(),merge(),map_values(),map_keys(),filter(),find(),for_each(),count(),is_empty(), andto_list(). - Sets --
set()constructor,add(),remove(),contains(),union(),intersection(),difference(),symmetric_diff(),is_subset(),is_superset(),to_list(),map(), andfilter(). - Tuples -- immutable, hashable bundles with
len(),contains(),to_list(), positional access, and destructuring. - Ranges -- lazy numeric sequences with O(1)
contains(),len(),to_list(),reverse(),map(),filter(), andfold(). - TypedArrays -- dense numeric arrays with vectorized ops:
sum(),mean(),std(),min(),max(),norm(),dot(),sort(),slice(),argmin(),argmax(),cumsum(), andabs(). - Records -- named immutable structs with
to_map(),with(), andfields(). - Tables -- columnar analytics with
col(),row(),filter(),sort_by(),select(),add_column(),group_by()+agg(),join(),pivot(),head(),tail(),to_csv(),to_json(), andto_string(). - Iterator protocol -- lazy evaluation with 15 adapters and 13 terminal operations.
- Broadcasting -- element-wise arithmetic on lists and typed arrays.
- Spread operator --
[...a, ...b]and{...defaults, ...overrides}for merging collections. - The pipe operator -- chaining collection operations with
|>for readable, left-to-right data pipelines. - Functional operations --
map(),filter(), andfold()with anonymous functions. - JSON interop --
json_parse()andjson_stringify(). - Agent patterns -- conversation history, skill registries, config layering, and result merging.
In the next chapter, you will learn how to handle errors gracefully so that your agents can recover from failures without crashing.
Exercises #
Exercise 7.1: Word Counter
Write a function count_words(sentence) that takes a string, splits it into a list of
words (you may assume words are separated by spaces), and returns a map where each key is
a word and each value is the number of times that word appears.
Hint: Iterate over the words list. For each word, check if the map already has that key. If it does, increment the count. If not, set it to 1.
Exercise 7.2: Inventory Tracker Create a list of maps representing products in an inventory:
let inventory = [
{"name": "Widget A", "price": 9.99, "quantity": 150},
{"name": "Widget B", "price": 24.50, "quantity": 30},
{"name": "Widget C", "price": 5.00, "quantity": 200},
{"name": "Widget D", "price": 49.99, "quantity": 8}
];
Write functions to: 1. Calculate the total inventory value (sum of price * quantity for all products). 2. Filter products with quantity below a given threshold (low-stock alert). 3. Transform the list into a map keyed by product name for fast lookup.
Exercise 7.3: Agent Message History Simulate an agent conversation history as a list of maps:
let history = [
{"role": "user", "content": "What is Neam?"},
{"role": "assistant", "content": "Neam is an agentic AI programming language."},
{"role": "user", "content": "How do I create an agent?"},
{"role": "assistant", "content": "Use the agent keyword with a provider and model."}
];
Write a function format_history(history) that returns a single string with each message
on its own line, formatted as [role]: content. Then write a function
count_by_role(history) that returns a map like {"user": 2, "assistant": 2}.
Exercise 7.4: Nested Data Traversal Given the following nested structure:
let university = {
"name": "Tech University",
"departments": [
{
"name": "Computer Science",
"courses": [
{"code": "CS101", "students": 120},
{"code": "CS201", "students": 85},
{"code": "CS301", "students": 45}
]
},
{
"name": "Mathematics",
"courses": [
{"code": "MATH101", "students": 200},
{"code": "MATH201", "students": 60}
]
}
]
};
Write a function total_students(university) that traverses the entire structure and
returns the sum of all students across all departments and courses.
Exercise 7.5: Agent Result Ranker
Write a function rank_results(results) that takes a list of maps, each with a
"score" key (a number between 0 and 1), and returns a new list sorted in descending
order by score. Use any sorting algorithm you are comfortable with (selection sort works
well for learning). Then write top_n(results, n) that returns only the first n items
from the ranked list.
Exercise 7.6: Set Operations -- Tag Analyzer An agent collects tags from two different data sources. Use set operations to analyze them:
let source_a_tags = ["ai", "agents", "neam", "llm", "rag", "python"];
let source_b_tags = ["neam", "rust", "llm", "wasm", "agents", "ml"];
Write a program that: 1. Converts both tag lists to sets. 2. Finds the common tags (tags that appear in both sources). 3. Finds the tags unique to Source A (in A but not in B). 4. Finds the tags unique to Source B (in B but not in A). 5. Finds the total unique tags across both sources (union). 6. Prints all four results with descriptive labels.
Expected output:
Common tags: {neam, llm, agents}
Unique to A: {ai, rag, python}
Unique to B: {rust, wasm, ml}
All unique tags: {ai, agents, neam, llm, rag, python, rust, wasm, ml}
Exercise 7.7: Data Pipeline with Pipe Use the pipe operator to build a data-processing pipeline. Start with this raw data:
let raw = [42, 17, 85, 23, 85, 42, 91, 17, 63, 42, 55, 91];
Chain the following operations using |>:
1. Remove duplicates with .unique().
2. Sort in ascending order with .sort().
3. Reverse to get descending order with .reverse().
4. Take the top 5 values with .take(5).
5. Join them into a comma-separated string with .join(", ").
Print the final result. Expected output:
Top 5: 91, 85, 63, 55, 42
Bonus: Modify the pipeline to also calculate and print the mean of the top 5 values.
Exercise 7.8: TypedArray -- Embedding Similarity Given two embeddings stored as TypedArrays, compute the cosine similarity:
let query = float_array([0.8, 0.2, 0.5, 0.1]);
let doc1 = float_array([0.7, 0.3, 0.4, 0.2]);
let doc2 = float_array([0.1, 0.9, 0.1, 0.8]);
Write a function cosine_sim(a, b) using .dot() and .norm(). Then rank doc1 and
doc2 by similarity to the query and print the results.
Exercise 7.9: Record -- Agent Configuration
Define a record type AgentSpec with fields name (string), model (string), and
temperature (number). Create a base spec, then use .with() to derive two variants:
one with a higher temperature for creative tasks and one with a different model for fast
tasks. Print all three configurations.
Exercise 7.10: Table -- Sales Analysis Create a table from this data:
let sales = table([
{"rep": "Alice", "product": "Widget", "amount": 500},
{"rep": "Bob", "product": "Gadget", "amount": 300},
{"rep": "Alice", "product": "Gadget", "amount": 450},
{"rep": "Bob", "product": "Widget", "amount": 600},
{"rep": "Carol", "product": "Widget", "amount": 350},
{"rep": "Carol", "product": "Gadget", "amount": 275}
]);
- Use
.group_by("rep").agg(...)to find each rep's total sales. - Sort the result by total sales descending.
- Use
.filter()to find all Widget sales above 400. - Export the grouped result as CSV.
Exercise 7.11: Iterator Pipeline Using the lazy iterator protocol, process a large range without materializing intermediate lists:
let result = range(1, 1001).iter()
.filter(fn(n) { return n % 3 == 0; }) // divisible by 3
.map(fn(n) { return n * n; }) // square each
.take_while(fn(n) { return n < 50000; }) // stop at 50000
.sum();
emit f"Sum: {result}";
Predict the output, then run it. Modify the pipeline to also count how many numbers were
included using .count() instead of .sum().
Exercise 7.12: Spread Operator -- Config Builder
Write a function build_agent_config(overrides) that starts with a base configuration map
and merges user-provided overrides using the spread operator. Test it with:
let config1 = build_agent_config({"temperature": 0.2});
let config2 = build_agent_config({"model": "gpt-4o-mini", "stream": true});