Programming Neam
📖 23 min read

Chapter 7: Collections #

💠 Why This Matters

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:

Type

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:

neam
{
  let fruits = ["apple", "banana", "cherry"];
  emit "Fruits: " + str(fruits);
}

Output:

text
Fruits: [apple, banana, cherry]

Lists can contain any mix of types:

neam
{
  let mixed = ["hello", 42, true, 3.14, nil];
  emit "Mixed: " + str(mixed);
}

An empty list is created with []:

neam
{
  let empty = [];
  emit "Empty list: " + str(empty);
  emit "Length: " + str(len(empty));
}

Output:

text
Empty list: []
Length: 0

Indexing #

Lists are zero-indexed. Use square bracket notation to access an element by its position:

neam
{
  let fruits = ["apple", "banana", "cherry", "date"];
  emit "First fruit: " + fruits[0];
  emit "Second fruit: " + fruits[1];
  emit "Last fruit: " + fruits[3];
}

Output:

text
First fruit: apple
Second fruit: banana
Last fruit: date
⚠️ Warning

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:

neam
{
  let fruits = ["apple", "banana", "cherry", "date"];
  emit "Number of fruits: " + str(len(fruits));
}

Output:

text
Number of fruits: 4

Modifying Lists #

Use push() to append an element to the end of a list:

neam
{
  let colors = ["red", "green"];
  push(colors, "blue");
  emit "Colors: " + str(colors);
  emit "Length: " + str(len(colors));
}

Output:

text
Colors: [red, green, blue]
Length: 3

You can also assign to a specific index to replace an element:

neam
{
  let scores = [90, 85, 78];
  scores[1] = 92;
  emit "Updated scores: " + str(scores);
}

Output:

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

neam
{
  let fruits = ["apple", "banana", "cherry", "date"];

  let i = 0;
  while (i < len(fruits)) {
    emit "  " + str(i + 1) + ". " + fruits[i];
    i = i + 1;
  }
}

Output:

text
  1. apple
  2. banana
  3. cherry
  4. date
💡 Tip

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:

neam
{
  let fruits = ["apple", "banana", "cherry", "date"];

  for (fruit in fruits) {
    emit "  - " + fruit;
  }
}

Output:

text
  - apple
  - banana
  - cherry
  - date

When you need both the index and the value, use enumerate():

neam
{
  let fruits = ["apple", "banana", "cherry", "date"];

  for (i, fruit in enumerate(fruits)) {
    emit "  " + str(i + 1) + ". " + fruit;
  }
}

Output:

text
  1. apple
  2. banana
  3. cherry
  4. date

You can also iterate a fixed number of times using range():

neam
{
  // Print a simple multiplication table for 5
  for (i in range(1, 6)) {
    emit "5 x " + str(i) + " = " + str(5 * i);
  }
}

Output:

text
5 x 1 = 5
5 x 2 = 10
5 x 3 = 15
5 x 4 = 20
5 x 5 = 25
📝 When to use which loop
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:

neam
{
  let stack = ["first", "second", "third"];
  let top = pop(stack);
  emit "Popped: " + top;
  emit "Remaining: " + str(stack);
}

Output:

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

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

text
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

neam
{
  let letters = ["a", "b", "c", "d"];
  let reversed = reverse(letters);
  emit "Reversed: " + str(reversed);
}

Output:

text
Reversed: [d, c, b, a]

sort() -- Sort a List

The sort() function sorts a list in ascending order:

neam
{
  let scores = [78, 95, 62, 88, 45];
  let sorted_scores = sort(scores);
  emit "Sorted: " + str(sorted_scores);
}

Output:

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

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

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

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

text
Sum:  694
Mean: 86.75
Min:  76
Max:  95
Count: 8

Sorting, Uniqueness, and Membership

neam
{
  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
}
📝 Note

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:

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

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

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

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

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

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

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

neam
{
  // 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}
}
🎯 Try It Yourself

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

text
[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]
10
30
50
70
🌎 Real-World Analogy

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.

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

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

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

🎯 Try It Yourself

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:

neam
{
  let person = {
    "name": "Alice",
    "age": 30,
    "city": "London"
  };
  emit "Person: " + str(person);
}

Output:

text
Person: {name: Alice, age: 30, city: London}

An empty map is created with {}:

neam
{
  let config = {};
  emit "Empty map: " + str(config);
}

Non-String Keys

Maps accept any hashable value as a key -- numbers, booleans, and tuples all work:

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

neam
{
  let person = {
    "name": "Alice",
    "age": 30,
    "city": "London"
  };
  emit "Name: " + person["name"];
  emit "Age: " + str(person["age"]);
}

Output:

text
Name: Alice
Age: 30

You can also use dot notation for keys that are valid identifiers:

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

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

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

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

text
No email on file.

You can also use the has_key() function for a more explicit check:

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

neam
{
  let config = {
    "provider": "openai",
    "model": "gpt-4o",
    "temperature": "0.7"
  };

  for (key in config) {
    emit key + " = " + str(config[key]);
  }
}

Output:

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

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

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

neam
{
  let settings = {"theme": "dark", "language": "en"};

  emit "Has theme: " + str(contains(settings, "theme"));
  emit "Has font: " + str(contains(settings, "font"));
}

Output:

text
Has theme: true
Has font: false

remove() -- Remove a Key

The remove() function removes a key-value pair from a map:

neam
{
  let data = {"a": 1, "b": 2, "c": 3};
  remove(data, "b");
  emit "After remove: " + str(data);
}

Output:

text
After remove: {a: 1, c: 3}

size() -- Count Entries

The size() function returns the number of key-value pairs:

neam
{
  let config = {"model": "gpt-4o", "temperature": 0.7};
  emit "Entries: " + str(size(config));
}

Output:

text
Entries: 2

entries() -- Get Key-Value Pairs

The entries() function returns a list of [key, value] pairs, useful for iteration:

neam
{
  let scores = {"Alice": 95, "Bob": 87, "Carol": 92};

  for (entry in entries(scores)) {
    emit entry[0] + " scored " + str(entry[1]);
  }
}

Output:

text
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()

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

neam
{
  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()

neam
{
  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()

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

🌎 Real-World Analogy

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:

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

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

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

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

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

1 2
5 6
1 2
3
4

Subset and Superset Tests #

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

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

neam
{
  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]
}
🎯 Try It Yourself

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:

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

text
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
.0
3.14
.0
true
.2
42

Tuple Methods #

Tuples support a small set of query methods:

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

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

Type
Example
Map
{k: v}
Agent config, user profile,
API response
Tuple
(a, b)
(x, y) coordinates,
(name, score) pairs,
RGB colors, map keys

Rules of thumb:

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 #

neam
{
  // 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
}
start
0
step
1

Range Methods #

Ranges support O(1) membership testing -- checking whether a number falls within the range does not require iterating through it:

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

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

neam
{
  // 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}";
}
0.9
true
0.9
0.7

Vectorized Operations #

TypedArrays support element-wise arithmetic and vector math without explicit loops:

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

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

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

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

neam
// 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}";
}
"x" --> 10
"y" --> 20
"z" --> ???

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:

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

text
Base:     model=gpt-4o, temp=0.7
Creative: model=gpt-4o, temp=0.95
Fast:     model=gpt-4o-mini, temp=0.3

Record Methods #

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

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

product
revenue
product
revenue

Accessing Rows and Columns #

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

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

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

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

product
avg_revenue

join() -- Combining Tables #

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

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

month
B

Export: to_csv(), to_json() #

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

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

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

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

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

neam
{
  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]
}
text
     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:

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

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

neam
{
  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}";
}
model
temperature
max_tokens
stream

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:

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

neam
{
  let team = [
    {"name": "Alice", "role": "Engineer"},
    {"name": "Bob", "role": "Designer"},
    {"name": "Carol", "role": "Manager"}
  ];

  for (member in team) {
    emit member.name + " -- " + member.role;
  }
}

Output:

text
Alice -- Engineer
Bob -- Designer
Carol -- Manager

Map of Lists #

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

text
Math courses:
  - Calculus
  - Linear Algebra
  - Statistics
CS courses:
  - Algorithms
  - Operating Systems
  - AI

Deeply Nested Structures #

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

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

Index
1
Hash
Table

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:

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

text
Total: 439
Average: 87.8

A string accumulation variant concatenates results:

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

text
Tags: ai, agents, neam, rag

Pattern 2: Filtering #

Create a new list containing only elements that satisfy a condition:

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

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

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

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

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

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

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

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

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

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

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

text
Sum: 15
Product: 120

Chaining Functional Operations #

These operations can be chained to build expressive data pipelines:

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

text
Affordable in-stock: [Widget A, Widget C]
Total in-stock value: $64.98
💡 Tip

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:

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

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

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

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

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

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

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

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

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

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

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:

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

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

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

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

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

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

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

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

neam
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}
]);
  1. Use .group_by("rep").agg(...) to find each rep's total sales.
  2. Sort the result by total sales descending.
  3. Use .filter() to find all Widget sales above 400.
  4. Export the grouped result as CSV.

Exercise 7.11: Iterator Pipeline Using the lazy iterator protocol, process a large range without materializing intermediate lists:

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

neam
let config1 = build_agent_config({"temperature": 0.2});
let config2 = build_agent_config({"model": "gpt-4o-mini", "stream": true});
Start typing to search...