Chapter 4: Variables, Types, and Operators #
"A variable is just a name attached to a value. The art is in choosing the right names and the right values."
In Chapter 3, you learned how to emit simple string literals. Real programs, however, need to store data, transform it, compare it, and make decisions based on it. This chapter introduces Neam's type system, variable declarations, and the operators that let you manipulate data.
By the end of this chapter, you will be able to:
- Declare variables with
letand constants withconst - Work with all thirteen core data types: Number, String, Boolean, Nil, List, Map, Tuple, Set, Range, Option, TypedArray, Record, and Table
- Use f-strings for readable string interpolation
- Understand type coercion and broadcasting
- Perform arithmetic, comparison, and logical operations
- Use the pipe operator (
|>), theinoperator, destructuring, slicing, and the spread operator - Convert between types using
str()and understand type inference - Use
typeof()to inspect types at runtime - Write programs that combine variables and operators to solve real problems
4.1 Variable Declaration with let #
A variable is a named container for a value. In Neam, you create variables with
the let keyword:
{
let greeting = "Hello";
let count = 42;
let pi = 3.14159;
let active = true;
emit greeting; // Hello
emit str(count); // 42
emit str(pi); // 3.14159
emit str(active); // true
}
The general form is:
let <name> = <value>;
Key rules:
- Variable names must start with a letter or underscore, followed by letters,
digits, or underscores. Examples:
count,user_name,_temp,x1. - Variable names are case-sensitive:
nameandNameare different variables. - You must initialize a variable when you declare it.
let x;without a value is not valid. - Variables declared with
letcan be reassigned:
{
let score = 0;
emit str(score); // 0
score = 100;
emit str(score); // 100
}
Naming Conventions #
Neam follows the snake_case convention by community practice:
| Style | Example | Use in Neam |
|---|---|---|
snake_case |
user_name, max_count |
Variables, functions |
PascalCase |
MyAgent, DocBot |
Agents, knowledge bases |
UPPER_SNAKE |
MAX_RETRIES |
Constants (by convention) |
Variable Scope #
Variables in Neam are block-scoped. A variable is visible from its declaration
to the end of the enclosing { ... } block. Inner blocks can see variables from
outer blocks, but not vice versa:
{
let outer = "I am visible everywhere in this block";
if (true) {
let inner = "I am only visible inside this if block";
emit outer; // Works -- outer is visible here
emit inner; // Works -- inner is visible here
}
emit outer; // Works
// emit inner; // ERROR: 'inner' is not defined in this scope
}
This is the same scoping model as JavaScript's let and Rust's let. If you are
coming from Python, where variables are function-scoped, this is an important
difference: in Neam, a variable declared inside an if, while, or for block
does not exist outside that block.
Reassignment vs. Redeclaration #
You can reassign a let variable, but you cannot redeclare it in the same scope:
{
let x = 10;
x = 20; // Reassignment -- this is fine
// let x = 30; // ERROR: Variable 'x' already declared in this scope
// But you CAN declare a new 'x' in a nested scope (shadowing)
if (true) {
let x = 30; // This is a NEW variable that shadows the outer 'x'
emit str(x); // 30
}
emit str(x); // 20 -- the outer 'x' is unchanged
}
Shadowing -- declaring a new variable with the same name in an inner scope -- is allowed but should be used sparingly, as it can make code harder to follow.
4.2 Constants with const #
When a value should never change after initialization, declare it with const:
{
const MAX_RETRIES = 3;
const APP_NAME = "NeamBot";
const PI = 3.14159;
emit APP_NAME; // NeamBot
emit str(MAX_RETRIES); // 3
}
Attempting to reassign a const will produce a compile-time error:
{
const LIMIT = 10;
LIMIT = 20; // ERROR: Cannot reassign constant 'LIMIT'
}
When to use const vs let:
- Use
constfor values that are fixed for the lifetime of the program: configuration values, mathematical constants, limits, labels. - Use
letfor values that will change: loop counters, accumulators, intermediate results.
A good rule of thumb: start with const. Switch to let only when you
discover you need to reassign the variable.
4.3 The Neam Type System #
Neam is dynamically typed with type inference. You never write type annotations -- the language figures out the type of each value automatically. There are thirteen core types, organized into three tiers:
Let us explore each one.
Number #
Neam has a single numeric type backed by a 64-bit IEEE 754 double-precision floating-point number. This means integers and decimals use the same type:
{
let integer = 42;
let decimal = 3.14;
let negative = -7;
let big = 1000000;
emit str(integer); // 42
emit str(decimal); // 3.14
emit str(negative); // -7
}
There is no separate int vs float distinction. The number 42 and the number
42.0 are the same type. Doubles can represent integers exactly up to 2^53
(about 9 quadrillion), so for virtually all practical purposes, you will not run
into precision issues with whole numbers.
String #
Strings are sequences of characters enclosed in double quotes:
{
let greeting = "Hello, World!";
let empty = "";
let with_spaces = " padded ";
let with_numbers = "Agent 007";
emit greeting;
emit "Length: " + str(len(greeting)); // Length: 13
}
Strings in Neam are immutable. Operations like concatenation create new strings rather than modifying the original.
String Operations #
Neam provides a rich set of string operations through both built-in functions and method syntax:
| Operation | Syntax | Example | Result |
|---|---|---|---|
| Concatenation | + |
"Hello" + " World" |
"Hello World" |
| Length | len(s) |
len("Hello") |
5 |
| Contains | s.contains(sub) |
"Hello".contains("ell") |
true |
| Starts with | starts_with(s, prefix) |
starts_with("Hello", "He") |
true |
| Ends with | ends_with(s, suffix) |
ends_with("Hello", "lo") |
true |
| Substring | s.substring(start, end) |
"Hello".substring(0, 3) |
"Hel" |
| Uppercase | s.upper() |
"hello".upper() |
"HELLO" |
| Lowercase | s.lower() |
"HELLO".lower() |
"hello" |
| Replace | replace(s, old, new) |
replace("hello", "l", "r") |
"herro" |
| Split | split(s, delim) |
split("a,b,c", ",") |
["a", "b", "c"] |
| Join | join(list, delim) |
join(["a", "b"], "-") |
"a-b" |
| Trim | trim(s) |
trim(" hi ") |
"hi" |
| To string | str(x) |
str(42) |
"42" |
{
let text = "Hello, World!";
emit str(len(text)); // 13
emit str(text.contains("World")); // true
emit text.substring(0, 5); // Hello
emit text.upper(); // HELLO, WORLD!
emit text.lower(); // hello, world!
}
Practical String Patterns #
Strings are the primary data type you will work with when building AI agents, because LLM responses are always strings. Here are patterns you will use frequently:
{
// Building prompts from parts
let topic = "machine learning";
let audience = "beginners";
let prompt = "Explain " + topic + " for " + audience + " in two sentences.";
emit prompt;
// Parsing structured output
let csv_line = "Alice,30,London";
let fields = split(csv_line, ",");
emit "Name: " + fields[0]; // Name: Alice
emit "Age: " + fields[1]; // Age: 30
emit "City: " + fields[2]; // City: London
// Checking response content
let response = "The answer is APPROVED for release.";
if (response.contains("APPROVED")) {
emit "Response was approved";
}
}
F-Strings (Formatted Strings) #
Neam supports f-strings for embedding expressions directly inside string
literals. Prefix the string with f and place expressions inside {...}:
{
let name = "Alice";
let age = 30;
// F-string interpolation -- much cleaner than concatenation
emit f"Hello, {name}!"; // Hello, Alice!
emit f"{name} is {age} years old"; // Alice is 30 years old
// Expressions are evaluated inside the braces
emit f"Next year: {age + 1}"; // Next year: 31
emit f"Name uppercased: {name.upper()}"; // Name uppercased: ALICE
}
Compare f-strings with the concatenation approach:
{
let lang = "Neam";
let edition = "Data Types";
// Without f-strings (verbose)
emit lang + " - " + edition + " Edition";
// With f-strings (concise and readable)
emit f"{lang} - {edition} Edition";
}
Both produce the same output: Neam - Data Types Edition. F-strings are
especially useful when building prompts or formatting output that mixes text with
multiple variables. Any valid expression can go inside {...}, including
function calls, method calls, and arithmetic.
Boolean #
Booleans represent truth values. There are exactly two: true and false.
{
let is_ready = true;
let is_done = false;
emit str(is_ready); // true
emit str(is_done); // false
}
Booleans are most commonly used in conditional logic (Chapter 6) and as the result of comparison operations.
Nil #
nil represents the absence of a value. It is Neam's equivalent of null,
None, or undefined in other languages.
{
let nothing = nil;
emit str(nothing); // nil
}
nil is falsy -- when used in a boolean context (like an if condition), it
behaves like false. This is useful for checking whether a value exists:
{
let result = nil;
if (!result) {
emit "No result yet";
}
}
List #
Lists are ordered, indexed collections of values. They can hold values of different types:
{
let numbers = [1, 2, 3, 4, 5];
let mixed = ["hello", 42, true, nil];
let empty_list = [];
emit str(numbers); // [1, 2, 3, 4, 5]
emit numbers[0]; // First element: 1 (zero-indexed)
emit str(numbers[2]); // Third element: 3
emit str(len(numbers)); // Length: 5
}
Lists are zero-indexed: the first element is at position 0, the second at
position 1, and so on.
{
let fruits = ["apple", "banana", "cherry", "date"];
// Access by index
emit "First: " + fruits[0]; // apple
emit "Last: " + fruits[3]; // date
emit "Count: " + str(len(fruits)); // 4
// Iterate with a for-in loop
let i = 1;
for fruit in fruits {
emit " " + str(i) + ". " + fruit;
i = i + 1;
}
}
Expected output:
First: apple
Last: date
Count: 4
1. apple
2. banana
3. cherry
4. date
List Operations #
| Operation | Syntax | Description |
|---|---|---|
| Create | [1, 2, 3] |
Literal syntax |
| Length | len(list) |
Number of elements |
| Access | list[i] |
Get element at index (zero-based) |
| Append | append(list, val) |
Add element to end |
| Push | push(list, val) |
Same as append |
| Pop | pop(list) |
Remove and return last element |
| Slice | slice(list, start, end) |
Extract a portion |
| Contains | contains(list, val) |
Check if value exists |
| Index of | index_of(list, val) |
Find position of value (-1 if not found) |
| Sort | sort(list) |
Sort in ascending order |
| Reverse | reverse(list) |
Reverse the order |
| Map | map(list, fn) |
Apply function to each element |
| Filter | filter(list, fn) |
Keep elements matching predicate |
{
let numbers = [5, 3, 8, 1, 9, 2];
// Modifying lists
append(numbers, 7);
emit str(numbers); // [5, 3, 8, 1, 9, 2, 7]
// Searching
emit str(contains(numbers, 8)); // true
emit str(index_of(numbers, 8)); // 2
// Sorting
let sorted = sort([5, 3, 8, 1]);
emit str(sorted); // [1, 3, 5, 8]
// Functional operations with anonymous functions
let doubled = map([1, 2, 3], fn(x) { return x * 2; });
emit str(doubled); // [2, 4, 6]
let evens = filter([1, 2, 3, 4, 5], fn(x) { return x % 2 == 0; });
emit str(evens); // [2, 4]
}
Lists in Neam are mutable -- append() and push() modify the list
in place. If you need an unmodified copy, assign the list to a new variable
before modifying.
Why Lists Matter for Agents #
Lists are central to agent programming. When an agent returns multiple results, they come back as a list. When you define handoff targets, they are a list of agents. When you build a RAG knowledge base, document chunks are stored in lists. Getting comfortable with list operations now will pay dividends throughout this book.
Map #
Maps are collections of key-value pairs. Keys are strings; values can be any type:
{
let person = {
"name": "Alice",
"age": 30,
"city": "London"
};
emit str(person); // {"name": "Alice", "age": 30, "city": "London"}
emit "Name: " + person["name"]; // Name: Alice
emit "Age: " + str(person["age"]); // Age: 30
}
Maps are also called dictionaries or hash maps in other languages. You access values using bracket notation with the key as a string.
{
let config = {
"host": "localhost",
"port": 8080,
"debug": true
};
emit "Server: " + config["host"] + ":" + str(config["port"]);
emit "Debug mode: " + str(config["debug"]);
}
Expected output:
Server: localhost:8080
Debug mode: true
Map Operations #
| Operation | Syntax | Description |
|---|---|---|
| Create | {"key": value} |
Literal syntax |
| Access | map["key"] |
Get value by key |
| Set | map["key"] = value |
Set or update a key-value pair |
| Length | len(map) |
Number of key-value pairs |
| Keys | keys(map) |
Get list of all keys |
| Values | values(map) |
Get list of all values |
| Has key | has_key(map, "key") |
Check if key exists |
| Delete | delete(map, "key") |
Remove a key-value pair |
{
let user = {
"name": "Alice",
"role": "engineer",
"level": 3
};
// Access and modify
emit user["name"]; // Alice
user["level"] = 4; // Update a value
user["department"] = "AI Research"; // Add a new key
// Inspect structure
emit str(len(user)); // 4
emit str(keys(user)); // ["name", "role", "level", "department"]
emit str(has_key(user, "email")); // false
}
Why Maps Matter for Agents #
Maps are the native data structure for agent communication. When an agent calls a tool, the parameters are a map. When an agent returns structured data, it is a map. JSON -- the lingua franca of LLM APIs -- maps directly to Neam's map type. The map you define in Neam is the exact structure that gets serialized to JSON when sent to an LLM provider:
{
// This map structure mirrors what a tool call looks like
let tool_params = {
"query": "climate change effects",
"max_results": 5,
"include_sources": true
};
emit str(tool_params);
// {"query": "climate change effects", "max_results": 5, "include_sources": true}
}
Nested Data Structures #
Lists and maps can be nested to represent complex data:
{
let team = {
"name": "AI Research",
"members": [
{"name": "Alice", "role": "lead"},
{"name": "Bob", "role": "engineer"},
{"name": "Carol", "role": "researcher"}
],
"active": true
};
emit "Team: " + team["name"];
emit "Lead: " + team["members"][0]["name"];
emit "Size: " + str(len(team["members"]));
}
Expected output:
Team: AI Research
Lead: Alice
Size: 3
Nested structures like this appear everywhere in agent programs -- agent configurations, tool responses, knowledge base results, and multi-agent handoff payloads all use nested maps and lists.
Tuple #
Tuples are immutable, fixed-size, ordered sequences. Unlike lists, tuples cannot be modified after creation -- you cannot append, remove, or change their elements. This makes them ideal for returning multiple values from a function or representing a fixed collection like a coordinate pair.
{
// Creating tuples with parentheses
let point = (3.14, 2.72);
let result = (true, "success", 42);
let single = (42,); // trailing comma distinguishes from grouping
// Access elements by position using .0, .1, .2, etc.
emit f"x = {point.0}"; // x = 3.14
emit f"y = {point.1}"; // y = 2.72
emit f"code = {result.2}"; // code = 42
}
The key difference between a tuple and a list:
┌───────────────────────────────────────────────────────────┐
│ List [1, 2, 3] │ Tuple (1, 2, 3) │
├───────────────────────────┼───────────────────────────────┤
│ Mutable (can change) │ Immutable (fixed) │
│ Variable length │ Fixed length │
│ list[0] access │ tuple.0 access │
│ Use for collections │ Use for grouped values │
└───────────────────────────┴───────────────────────────────┘
Tuple Destructuring #
You can unpack a tuple's elements into separate variables in a single statement:
{
let point = (3.14, 2.72);
// Destructure into individual variables
let (x, y) = point;
emit f"x = {x}"; // x = 3.14
emit f"y = {y}"; // y = 2.72
// Works with any number of elements
let (name, age) = ("Alice", 30);
emit f"{name} is {age} years old"; // Alice is 30 years old
}
Tuple destructuring is especially useful when a function returns multiple values. You will see this pattern frequently in later chapters.
Tuple Operations #
| Operation | Syntax | Description |
|---|---|---|
| Create | (val1, val2, ...) |
Literal syntax |
| Access | tuple.0, tuple.1 |
Positional access (zero-based) |
| Length | tuple.len() |
Number of elements |
| Contains | tuple.contains(val) |
Check if value exists |
| To list | tuple.to_list() |
Convert to a mutable list |
| Destructure | let (a, b) = tuple; |
Unpack into variables |
| Equality | (1, 2) == (1, 2) |
Structural equality (true) |
Set #
Sets are unordered collections of unique elements. If you add a duplicate, it is silently ignored. Sets are optimized for fast membership testing and for operations from set theory: union, intersection, and difference.
{
// Create a set from a list using .to_set()
let words = ["hello", "world", "hello", "neam", "world"];
let unique_words = words.to_set();
emit unique_words; // {"hello", "world", "neam"} (order may vary)
// Duplicates are automatically removed
let numbers = [1, 2, 2, 3, 3, 3].to_set();
emit numbers; // {1, 2, 3}
}
Set Operations #
Sets support the classic set-algebra operations:
{
let a = set(1, 2, 3, 4);
let b = set(3, 4, 5, 6);
let u = a.union(b); // {1, 2, 3, 4, 5, 6}
let i = a.intersection(b); // {3, 4}
let d = a.difference(b); // {1, 2}
emit f"Union: {u}";
emit f"Intersection: {i}";
emit f"Difference: {d}";
}
| Operation | Syntax | Description |
|---|---|---|
| Create | set(val1, val2, ...) |
Constructor syntax |
| From list | list.to_set() |
Convert list to set |
| Add | s.add(val) |
Add an element |
| Remove | s.remove(val) |
Remove an element |
| Contains | s.contains(val) |
O(1) membership test |
| Length | s.len() |
Number of elements |
| Union | a.union(b) |
All elements from both sets |
| Intersection | a.intersection(b) |
Elements common to both |
| Difference | a.difference(b) |
Elements in a but not b |
| To list | s.to_list() |
Convert to a list |
Why Sets Matter for Agents #
When an agent receives search results from multiple sources, you often need to deduplicate entities. Sets make this trivial:
{
let source_a = ["Alice", "Bob", "Carol"];
let source_b = ["Bob", "Dave", "Carol"];
let all_names = source_a.to_set().union(source_b.to_set());
emit f"Unique people: {all_names}";
// Unique people: {"Alice", "Bob", "Carol", "Dave"}
}
Range #
Ranges represent lazy sequences of integers. They do not allocate a list of all values upfront -- instead, they produce values on demand as you iterate. This makes them memory-efficient even for very large sequences.
{
// range(n) produces 0, 1, 2, ..., n-1
for i in range(5) {
emit str(i);
}
// Output: 0 1 2 3 4
}
There are three forms of range():
{
// Fibonacci using range
let fibs = [0, 1];
for i in range(8) {
let n = fibs.len();
let a = fibs[n - 1];
let b = fibs[n - 2];
fibs.push(a + b);
}
emit f"Fibonacci (first 10): {fibs}";
// Fibonacci (first 10): [0, 1, 1, 2, 3, 5, 8, 13, 21, 34]
}
Ranges are lazy -- range(1000000) does not create a million-element list in
memory. It produces each value only when the loop asks for the next one. This is
important for performance in data-heavy agent programs.
| Operation | Syntax | Description |
|---|---|---|
| Count from zero | range(n) |
0 to n-1 |
| Start and end | range(start, end) |
start to end-1 |
| With step | range(start, end, step) |
start, start+step, ..., less than end |
| Membership | 5 in range(10) |
Check if value is in range |
Option #
The Option type represents a value that might or might not exist. Instead of
using nil directly (which can cause unexpected errors if you forget to check),
Option wraps the presence or absence of a value in a safe container:
Some(value)-- a value is presentNone-- no value is present
{
// Creating Option values
let maybe = Some(42);
emit f"Some(42).unwrap() = {maybe.unwrap()}"; // 42
// None represents the absence of a value
emit f"None.unwrap_or(0) = {None.unwrap_or(0)}"; // 0
}
Where Options Appear #
Many operations that might fail return Option instead of crashing. For example,
accessing the first element of an empty list:
{
// .first() returns Option -- safe even on empty lists
let empty_result = [].first();
emit f"[].first() = {empty_result}"; // None
let has_data = [1, 2, 3].first();
emit f"[1,2,3].first() = {has_data}"; // Some(1)
}
Option Methods #
| Method | Syntax | Description |
|---|---|---|
| Unwrap | opt.unwrap() |
Get the value (errors if None) |
| Unwrap or | opt.unwrap_or(default) |
Get the value, or default if None |
| Map | opt.map(fn) |
Transform the inner value if present |
| Is some | opt == None |
Check if None |
{
// unwrap_or provides a safe default
let result = None;
let value = result.unwrap_or("no data");
emit f"Value: {value}"; // Value: no data
// .map() transforms the inner value without unwrapping
let maybe_num = Some(10);
let doubled = maybe_num.map(fn(x) { return x * 2; });
emit f"Doubled: {doubled}"; // Doubled: Some(20)
}
Why Option Matters for Agents #
When an agent calls a tool that might fail (network timeout, missing data, invalid
query), the tool can return None instead of crashing the entire program. You can
then handle the missing data gracefully:
{
// Simulating a tool that may or may not return data
let search_result = [].first(); // None -- no results found
let answer = search_result.unwrap_or("No results found");
emit f"Answer: {answer}"; // Answer: No results found
}
This pattern replaces the manual nil checking you saw in Section 4.17 with a
more structured, less error-prone approach.
nil is a raw value meaning "nothing." It behaves like null in other
languages — any variable can be nil, and you must remember to check for it.
None is a typed Option variant meaning "no value is present." It belongs
to the Option type alongside Some(value) and carries methods like
.unwrap_or() and .map().
Use nil for simple sentinel checks (if (x == nil)). Use Option / None
when you want the compiler and method chain to enforce safe value handling.
TypedArray #
A TypedArray is a homogeneous numeric array optimized for mathematical computation. Unlike a regular List (which can hold mixed types), a TypedArray holds only numbers of a single kind -- either floating-point or integer -- and supports vectorized operations that run much faster than looping over a List.
{
// Create typed arrays with constructor functions
let floats = float_array([1.0, 2.0, 3.0, 4.0, 5.0]);
let ints = int_array([10, 20, 30, 40, 50]);
emit f"floats: {floats}"; // [1.0, 2.0, 3.0, 4.0, 5.0]
emit f"ints: {ints}"; // [10, 20, 30, 40, 50]
}
Vectorized Operations #
TypedArrays support element-wise arithmetic without explicit loops. Operations apply to every element simultaneously:
{
let a = float_array([1.0, 2.0, 3.0]);
let b = float_array([4.0, 5.0, 6.0]);
// Element-wise arithmetic
emit f"a + b = {a + b}"; // [5.0, 7.0, 9.0]
emit f"a * b = {a * b}"; // [4.0, 10.0, 18.0]
emit f"a * 10 = {a * 10}"; // [10.0, 20.0, 30.0] (broadcasting)
emit f"a + 100 = {a + 100}"; // [101.0, 102.0, 103.0]
}
Statistical and Mathematical Methods #
TypedArrays have built-in methods for common numerical operations:
{
let data = float_array([4.0, 8.0, 15.0, 16.0, 23.0, 42.0]);
emit f"sum: {data.sum()}"; // 108.0
emit f"mean: {data.mean()}"; // 18.0
emit f"std: {data.std()}"; // standard deviation
emit f"min: {data.min()}"; // 4.0
emit f"max: {data.max()}"; // 42.0
emit f"argmin: {data.argmin()}"; // 0 (index of min)
emit f"argmax: {data.argmax()}"; // 5 (index of max)
// Cumulative sum
emit f"cumsum: {data.cumsum()}"; // [4.0, 12.0, 27.0, 43.0, 66.0, 108.0]
// Absolute value
let neg = float_array([-3.0, -1.0, 2.0, -4.0]);
emit f"abs: {neg.abs()}"; // [3.0, 1.0, 2.0, 4.0]
}
TypedArray Methods Reference #
| Method | Syntax | Description |
|---|---|---|
| Sum | arr.sum() |
Sum of all elements |
| Mean | arr.mean() |
Arithmetic mean |
| Std | arr.std() |
Standard deviation |
| Min | arr.min() |
Minimum value |
| Max | arr.max() |
Maximum value |
| Norm | arr.norm() |
Euclidean norm (L2) |
| Dot | arr.dot(other) |
Dot product of two arrays |
| Sort | arr.sort() |
Return sorted copy |
| Slice | arr.slice(start, end) |
Extract a portion |
| Argmin | arr.argmin() |
Index of minimum value |
| Argmax | arr.argmax() |
Index of maximum value |
| Cumsum | arr.cumsum() |
Cumulative sum |
| Abs | arr.abs() |
Element-wise absolute value |
Dot Product and Norm #
For linear algebra operations, TypedArrays provide dot() and norm():
{
let v1 = float_array([1.0, 2.0, 3.0]);
let v2 = float_array([4.0, 5.0, 6.0]);
emit f"dot product: {v1.dot(v2)}"; // 32.0 (1*4 + 2*5 + 3*6)
emit f"norm of v1: {v1.norm()}"; // 3.7416... (sqrt(1+4+9))
}
Why TypedArrays Matter for Agents #
Embeddings -- the dense numeric vectors at the heart of semantic search, RAG retrieval, and similarity scoring -- are naturally represented as TypedArrays. Computing cosine similarity between two embeddings is a dot product divided by the product of their norms:
{
let embed_a = float_array([0.1, 0.3, 0.5, 0.7]);
let embed_b = float_array([0.2, 0.4, 0.4, 0.6]);
let similarity = embed_a.dot(embed_b) / (embed_a.norm() * embed_b.norm());
emit f"Cosine similarity: {similarity}";
}
Record #
A Record is a named tuple -- a lightweight, immutable struct with named
fields. Records give you the structure of a Map with the immutability and
performance of a Tuple. They are defined with the record keyword:
// Define a record type
record Point { x: number, y: number }
// Define another record type
record Person { name: string, age: number, city: string }
{
// Create instances by calling the record name as a constructor
let p = Point(10, 20);
let alice = Person("Alice", 30, "London");
emit f"Point: ({p.x}, {p.y})"; // Point: (10, 20)
emit f"Person: {alice.name}, age {alice.age}"; // Person: Alice, age 30
}
Records are immutable -- you cannot change a field after creation. To get a
modified copy, use the with() method:
record Point { x: number, y: number }
{
let p1 = Point(10, 20);
let p2 = p1.with(x: 50); // New record with x changed
emit f"p1 = ({p1.x}, {p1.y})"; // p1 = (10, 20) -- unchanged
emit f"p2 = ({p2.x}, {p2.y})"; // p2 = (50, 20) -- new copy
}
Record Methods #
| Method | Syntax | Description |
|---|---|---|
| Fields | rec.fields() |
List of field names |
| To map | rec.to_map() |
Convert to a mutable Map |
| With | rec.with(field: val) |
Return a copy with one field changed |
| From map (static) | RecordType.from_map(map) |
Create record from a Map |
record Color { r: number, g: number, b: number }
{
let red = Color(255, 0, 0);
// Inspect fields
emit f"fields: {red.fields()}"; // ["r", "g", "b"]
// Convert to map
let m = red.to_map();
emit f"as map: {m}"; // {"r": 255, "g": 0, "b": 0}
// Create from a map
let blue = Color.from_map({"r": 0, "g": 0, "b": 255});
emit f"blue.b = {blue.b}"; // 255
}
Structural Equality and Hashing #
Records support structural equality -- two records are equal if they have the same type and the same field values. They are also hashable, which means they can be used as Set elements or Map keys:
record Point { x: number, y: number }
{
let a = Point(1, 2);
let b = Point(1, 2);
let c = Point(3, 4);
emit f"a == b: {a == b}"; // true (same fields, same values)
emit f"a == c: {a == c}"; // false
// Records can be stored in sets (because they are hashable)
let points = set(Point(0, 0), Point(1, 1), Point(0, 0));
emit f"unique points: {points.len()}"; // 2 (duplicate removed)
}
Why Records Matter for Agents #
Records are ideal for representing structured domain objects that flow through agent pipelines -- search results, tool parameters, parsed entities. Their immutability guarantees that data passed between agents cannot be accidentally modified, and their named fields make code self-documenting:
record SearchResult { title: string, url: string, score: number }
{
let result = SearchResult("Neam Language", "https://neam.dev", 0.95);
emit f"Top result: {result.title} (score: {result.score})";
}
Table #
A Table is a columnar data structure for working with tabular data -- like a spreadsheet or a database result set. Tables are built from maps where each key is a column name and each value is a list of column data:
{
let students = table({
"name": ["Alice", "Bob", "Carol", "Dave"],
"score": [92, 85, 78, 95],
"grade": ["A", "B", "C", "A"]
});
emit students.to_string();
}
Output:
Accessing Table Data #
{
let t = table({
"name": ["Alice", "Bob", "Carol"],
"age": [30, 25, 28],
"city": ["London", "NYC", "Berlin"]
});
// Access a column (returns a list)
emit f"Names: {t.col("name")}"; // ["Alice", "Bob", "Carol"]
// Access a row (returns a map)
emit f"Row 0: {t.row(0)}"; // {"name": "Alice", "age": 30, "city": "London"}
// Preview data
emit t.head(2).to_string(); // First 2 rows
emit t.tail(1).to_string(); // Last 1 row
}
Filtering, Sorting, and Selecting #
Tables support a fluent API for data manipulation:
{
let sales = table({
"product": ["Widget", "Gadget", "Widget", "Gizmo", "Gadget"],
"region": ["East", "West", "West", "East", "East"],
"revenue": [100, 250, 150, 300, 200]
});
// Filter rows
let big_sales = sales.filter(fn(row) { return row["revenue"] >= 200; });
emit big_sales.to_string();
// Sort by column
let sorted = sales.sort_by("revenue");
emit sorted.to_string();
// Select specific columns
let summary = sales.select(["product", "revenue"]);
emit summary.to_string();
}
Adding Columns and Grouping #
{
let t = table({
"item": ["A", "B", "C"],
"price": [10, 20, 30],
"qty": [5, 3, 7]
});
// Add a computed column
let with_total = t.add_column("total", fn(row) {
return row["price"] * row["qty"];
});
emit with_total.to_string();
// Group by and aggregate
let sales = table({
"region": ["East", "West", "East", "West"],
"revenue": [100, 200, 150, 250]
});
let by_region = sales.group_by("region");
emit f"Grouped: {by_region}";
}
Table Methods Reference #
| Method | Syntax | Description |
|---|---|---|
| Column | t.col(name) |
Get a column as a list |
| Row | t.row(i) |
Get a row as a map |
| Filter | t.filter(fn) |
Keep rows matching predicate |
| Sort by | t.sort_by(col) |
Sort rows by a column |
| Select | t.select(cols) |
Keep only named columns |
| Add column | t.add_column(name, fn) |
Add a computed column |
| Group by | t.group_by(col) |
Group rows by column value |
| Join | t.join(other, on) |
Join two tables on a key column |
| Pivot | t.pivot(row, col, val) |
Pivot to wide format |
| Head | t.head(n) |
First n rows |
| Tail | t.tail(n) |
Last n rows |
| To CSV | t.to_csv() |
Export as CSV string |
| To JSON | t.to_json() |
Export as JSON string |
| To string | t.to_string() |
Pretty-print as ASCII table |
Why Tables Matter for Agents #
Agents frequently work with structured datasets -- API responses, database queries, CSV imports, analytics results. Tables provide a natural way to manipulate this data without external libraries:
{
// Imagine an agent that fetches sales data from a tool
let results = table({
"customer": ["Acme", "Globex", "Initech", "Acme", "Globex"],
"amount": [500, 750, 300, 600, 400],
"status": ["paid", "pending", "paid", "pending", "paid"]
});
// Agent pipeline: filter paid, sort by amount, take top 3
let top_paid = results
.filter(fn(row) { return row["status"] == "paid"; })
.sort_by("amount")
.head(3);
emit top_paid.to_string();
}
4.4 Type Inference #
Neam uses type inference -- you never need to write type annotations. The compiler and VM determine the type of each value from the value itself:
{
let x = 42; // Number -- inferred from the literal 42
let s = "hello"; // String -- inferred from the double-quoted literal
let b = true; // Boolean -- inferred from the keyword true
let n = nil; // Nil -- inferred from the keyword nil
let l = [1, 2, 3]; // List -- inferred from the bracket syntax
let m = {"a": 1}; // Map -- inferred from the brace syntax
}
This makes Neam code concise without sacrificing clarity. Compare with a hypothetical statically-typed version:
// Hypothetical -- NOT real Neam syntax
let x: Number = 42;
let s: String = "hello";
Neam's approach removes the ceremony while keeping the semantics clear. If you
ever need to check a type at runtime, use typeof().
4.5 Type Checking with typeof() #
The typeof() function returns a string describing the type of any value:
{
emit typeof(42); // "number"
emit typeof(3.14); // "number"
emit typeof("hello"); // "string"
emit typeof(true); // "boolean"
emit typeof(nil); // "nil"
emit typeof([1, 2]); // "list"
emit typeof({"a": 1}); // "map"
}
This is useful for debugging and for writing functions that handle different types of input:
fun describe(value) {
let t = typeof(value);
if (t == "number") {
return "It's a number: " + str(value);
}
if (t == "string") {
return "It's a string: " + value;
}
if (t == "boolean") {
return "It's a boolean: " + str(value);
}
return "It's a " + t;
}
{
emit describe(42);
emit describe("hello");
emit describe(true);
emit describe([1, 2, 3]);
}
Expected output:
It's a number: 42
It's a string: hello
It's a boolean: true
It's a list
4.6 Arithmetic Operators #
Neam provides the standard arithmetic operators for working with numbers:
| Operator | Name | Example | Result |
|---|---|---|---|
+ |
Addition | 10 + 5 |
15 |
- |
Subtraction | 10 - 3 |
7 |
* |
Multiplication | 4 * 6 |
24 |
/ |
Division | 15 / 3 |
5 |
% |
Modulo (remainder) | 17 % 5 |
2 |
- (unary) |
Negation | -42 |
-42 |
{
let a = 10;
let b = 3;
emit "a + b = " + str(a + b); // 13
emit "a - b = " + str(a - b); // 7
emit "a * b = " + str(a * b); // 30
emit "a / b = " + str(a / b); // 3.333...
emit "a % b = " + str(a % b); // 1
emit "-a = " + str(-a); // -10
}
Operator Precedence #
Neam follows standard mathematical precedence:
- Unary negation (
-x) -- highest - Multiplication, division, modulo (
*,/,%) - Addition, subtraction (
+,-) -- lowest
Use parentheses to override precedence when needed:
{
emit str(2 + 3 * 4); // 14 (multiplication first)
emit str((2 + 3) * 4); // 20 (parentheses override)
}
The + Operator: Overloaded #
The + operator has context-dependent behavior:
- Number + Number: Arithmetic addition.
3 + 4produces7. - String + String: Concatenation.
"Hello" + " World"produces"Hello World". - String + Number: Auto-converts the number to a string and concatenates.
"Count: " + 42produces"Count: 42". - Number + Boolean: Auto-converts
trueto1andfalseto0.10 + trueproduces11.
{
let count = 42;
// String + Number -- auto-coercion
emit "Count: " + count; // Count: 42
emit "Score: " + 95.5; // Score: 95.5
// Number + Boolean -- bool converts to 0/1
emit str(10 + true); // 11
emit str(10 + false); // 10
// You can still use str() for explicitness
emit "Value: " + str(count); // Value: 42
}
The * Operator: String Repetition #
When * is used with a String and a Number, it repeats the string:
{
emit "ha" * 3; // hahaha
emit "-" * 20; // --------------------
emit "abc" * 0; // (empty string)
}
Type Coercion Summary #
Division by Zero #
Dividing by zero does not crash the program. Because Neam numbers are IEEE 754
doubles, division by zero produces inf (infinity) or nan (not-a-number):
{
emit str(1 / 0); // inf
emit str(-1 / 0); // -inf
emit str(0 / 0); // nan
}
4.7 Comparison Operators #
Comparison operators compare two values and return a Boolean (true or false):
| Operator | Name | Example | Result |
|---|---|---|---|
== |
Equal | 5 == 5 |
true |
!= |
Not equal | 5 != 3 |
true |
< |
Less than | 3 < 5 |
true |
> |
Greater than | 5 > 3 |
true |
<= |
Less or equal | 5 <= 5 |
true |
>= |
Greater or equal | 5 >= 6 |
false |
{
let score = 85;
emit str(score == 100); // false
emit str(score != 100); // true
emit str(score > 80); // true
emit str(score < 90); // true
emit str(score >= 85); // true
emit str(score <= 84); // false
}
Equality Semantics #
- Numbers are compared by value:
42 == 42istrue. - Strings are compared by content:
"abc" == "abc"istrue. - Booleans are compared by value:
true == trueistrue. - Nil is only equal to itself:
nil == nilistrue,nil == 0isfalse.
4.8 Logical Operators #
Logical operators combine or negate Boolean values:
| Operator | Name | Description |
|---|---|---|
&& |
AND | true only if both operands are true |
\|\| |
OR | true if at least one operand is true |
! |
NOT | Inverts the Boolean value |
{
let a = true;
let b = false;
emit str(a && b); // false
emit str(a || b); // true
emit str(!a); // false
emit str(!b); // true
}
Short-Circuit Evaluation #
Neam uses short-circuit evaluation for && and ||:
&&stops and returnsfalseas soon as it finds afalseoperand.||stops and returnstrueas soon as it finds atrueoperand.
This means the right-hand side may not be evaluated at all:
{
let x = 0;
// The second condition is never checked because the first is false
let result = (x > 5) && (x < 10);
emit str(result); // false
}
Combining Comparisons with Logic #
The real power of logical operators comes from combining comparisons:
{
let age = 25;
let has_license = true;
// Can this person drive?
let can_drive = (age >= 16) && has_license;
emit "Can drive: " + str(can_drive); // true
// Is the age out of the typical range?
let unusual_age = (age < 0) || (age > 150);
emit "Unusual age: " + str(unusual_age); // false
}
4.9 The Pipe Operator (|>) #
When you chain multiple operations on data, the traditional approach nests function calls inside each other, which is read from the inside out:
{
// Nested calls -- read inside-out (hard to follow)
let data = [5, 3, 1, 4, 2, 3, 5, 1];
let result = reverse(sort(unique(data)));
emit str(result);
}
The pipe operator (|>) lets you write the same chain left-to-right,
matching the natural reading order:
{
// Pipe operator -- read left-to-right (easy to follow)
let data = [5, 3, 1, 4, 2, 3, 5, 1];
let result = data |> .unique() |> .sort() |> .reverse() |> .take(3);
emit str(result); // [5, 4, 3]
}
The pipe operator takes the value on the left and passes it as the receiver of the method call on the right. Each step's output becomes the next step's input:
┌──────────────────────────────────────────────────────────────┐
│ Pipe Operator Flow │
│ │
│ data ──→ .unique() ──→ .sort() ──→ .reverse() ──→ .take(3) │
│ │
│ [5,3,1,4,2,3,5,1] │
│ │ │
│ ▼ │
│ [5,3,1,4,2] (duplicates removed) │
│ │ │
│ ▼ │
│ [1,2,3,4,5] (sorted ascending) │
│ │ │
│ ▼ │
│ [5,4,3,2,1] (reversed) │
│ │ │
│ ▼ │
│ [5,4,3] (first 3 elements) │
│ │
└──────────────────────────────────────────────────────────────┘
The pipe operator is especially powerful for agent data processing. When you receive search results and need to filter, sort, and limit them, a pipeline reads like a recipe:
{
// Pipe operator chains transformations left-to-right
let scores = [85, 92, 78, 95, 88, 91, 76, 89];
// Without pipes -- nested calls are hard to read
let top3 = sort(filter(scores, fn(s) { return s >= 85; }));
// With pipes -- reads like a data processing recipe
let top3_piped = scores
|> filter(fn(s) { return s >= 85; })
|> sort()
|> slice(0, 3);
emit f"Top 3 scores: {top3_piped}"; // [95, 92, 91]
}
You can also use method calls for aggregate operations:
{
let scores = [85, 92, 78, 95, 88, 91, 76, 89];
emit f"Count: {scores.len()}";
emit f"Sum: {scores.sum()}";
emit f"Mean: {scores.mean()}";
emit f"Min: {scores.min()}";
emit f"Max: {scores.max()}";
}
4.10 Broadcasting #
Broadcasting lets you apply an arithmetic operator between a collection and a scalar, or between two collections of the same length. The operation is applied element-wise, producing a new collection of the same size.
Scalar Broadcasting #
When you combine a List (or TypedArray) with a single number, the number is "broadcast" to every element:
{
let prices = [10, 20, 30, 40, 50];
// Add a scalar to every element
emit f"prices + 5 = {prices + 5}"; // [15, 25, 35, 45, 55]
// Multiply every element by a scalar
emit f"prices * 2 = {prices * 2}"; // [20, 40, 60, 80, 100]
// Works with subtraction and division too
emit f"prices - 10 = {prices - 10}"; // [0, 10, 20, 30, 40]
emit f"prices / 10 = {prices / 10}"; // [1, 2, 3, 4, 5]
}
Element-Wise Operations #
When you combine two Lists (or TypedArrays) of the same length, the operation is applied to matching pairs of elements:
{
let a = [1, 2, 3];
let b = [4, 5, 6];
emit f"a + b = {a + b}"; // [5, 7, 9]
emit f"a * b = {a * b}"; // [4, 10, 18]
emit f"b - a = {b - a}"; // [3, 3, 3]
}
Broadcasting works on both List and TypedArray. Each collection type preserves its own type through broadcasts — a List operation returns a List, and a TypedArray operation returns a TypedArray. For heavy numeric work, prefer TypedArrays because they use optimized low-level SIMD operations:
{
let v = float_array([1.0, 2.0, 3.0]);
emit f"v * 10 = {v * 10}"; // [10.0, 20.0, 30.0]
emit f"v + v = {v + v}"; // [2.0, 4.0, 6.0]
emit f"v + float_array([10.0, 20.0, 30.0]) = {v + float_array([10.0, 20.0, 30.0])}";
// [11.0, 22.0, 33.0]
}
4.11 The in Operator #
The in operator tests whether a value is contained in a collection. It
returns true or false:
{
// in with lists
emit f"3 in [1,2,3]: {3 in [1, 2, 3]}"; // true
emit f"9 in [1,2,3]: {9 in [1, 2, 3]}"; // false
// in with sets
let colors = set("red", "green", "blue");
emit f"'red' in colors: {"red" in colors}"; // true
// in with ranges (O(1) -- does not iterate)
emit f"5 in range(10): {5 in range(10)}"; // true
emit f"15 in range(10): {15 in range(10)}"; // false
// in with strings (substring search)
emit f"'lo' in 'Hello': {"lo" in "Hello"}"; // true
emit f"'xyz' in 'Hello': {"xyz" in "Hello"}"; // false
// in with maps (checks keys, not values)
let config = {"host": "localhost", "port": 8080};
emit f"'host' in config: {"host" in config}"; // true
emit f"'debug' in config: {"debug" in config}"; // false
}
Use not in for the negation:
{
let banned = ["spam", "scam", "phish"];
let word = "hello";
if (word not in banned) {
emit f"'{word}' is allowed";
}
}
The in operator works with lists, sets, ranges, strings, and maps (checking
keys). It is a cleaner alternative to calling .contains() and reads more
naturally in if conditions and loop guards.
4.12 Destructuring Assignment #
Destructuring lets you unpack a collection into individual variables in a single statement. This reduces boilerplate and makes your intent clear.
List Destructuring #
{
let [first, second, ...rest] = [1, 2, 3, 4, 5];
emit f"first = {first}"; // first = 1
emit f"second = {second}"; // second = 2
emit f"rest = {rest}"; // rest = [3, 4, 5]
}
The ...rest syntax is the spread operator -- it captures all remaining
elements into a new list. The spread variable must appear last.
Tuple Destructuring #
{
let (name, age) = ("Alice", 30);
emit f"name = {name}"; // name = Alice
emit f"age = {age}"; // age = 30
}
Map Destructuring #
You can unpack map values into variables by matching on key names:
{
let person = {"name": "Alice", "age": 30, "city": "London"};
// Destructure specific keys from the map
let {name, age} = person;
emit f"name = {name}"; // name = Alice
emit f"age = {age}"; // age = 30
}
The variable names must match the keys in the map. You do not need to destructure every key -- you can pick only the ones you need.
Practical Example #
Destructuring is especially useful when iterating over structured data:
{
// Enumerate returns (index, value) tuples
for item in ["alpha", "beta", "gamma"].enumerate() {
emit f" {item.0}: {item.1}";
}
}
Expected output:
0: alpha
1: beta
2: gamma
{
// Zip pairs up elements from two lists
let keys_list = ["name", "age", "city"];
let vals = ["Bob", "25", "NYC"];
for pair in keys_list.zip(vals) {
emit f" {pair.0} = {pair.1}";
}
}
Expected output:
name = Bob
age = 25
city = NYC
4.13 The Spread Operator (...) #
The spread operator (...) expands a collection into individual elements. You
have already seen it in destructuring (let [first, ...rest] = list), but it
is also powerful for constructing new collections.
Spreading Lists #
Use ... inside list literals to merge lists together:
{
let front = [1, 2, 3];
let back = [7, 8, 9];
// Merge two lists
let combined = [...front, ...back];
emit f"combined = {combined}"; // [1, 2, 3, 7, 8, 9]
// Insert elements between spreads
let with_middle = [...front, 4, 5, 6, ...back];
emit f"with_middle = {with_middle}"; // [1, 2, 3, 4, 5, 6, 7, 8, 9]
// Prepend or append
let prepended = [0, ...front];
emit f"prepended = {prepended}"; // [0, 1, 2, 3]
}
Spreading Maps #
Use ... inside map literals to merge maps. When keys collide, later values
win:
{
let defaults = {"theme": "light", "font_size": 14, "lang": "en"};
let overrides = {"theme": "dark", "font_size": 18};
// Merge maps -- overrides win on collisions
let config = {...defaults, ...overrides};
emit f"config = {config}";
// {"theme": "dark", "font_size": 18, "lang": "en"}
}
The spread operator is especially useful for building agent configurations where you want to start with sensible defaults and selectively override:
{
let base_config = {
"provider": "openai",
"model": "gpt-4o",
"temperature": 0.7,
"max_tokens": 1000
};
// Create a variant with lower temperature
let precise_config = {...base_config, "temperature": 0.1};
emit f"precise temp: {precise_config["temperature"]}"; // 0.1
emit f"model: {precise_config["model"]}"; // gpt-4o
}
4.14 Slicing #
Slicing extracts a portion of a list or string using [start:end] or
[start:end:step] syntax. This is similar to Python's slicing notation.
{
let items = [10, 20, 30, 40, 50, 60, 70, 80];
emit f"items[2:5] = {items[2:5]}"; // [30, 40, 50]
emit f"items[::2] = {items[::2]}"; // [10, 30, 50, 70] (every other)
emit f"items[:3] = {items[:3]}"; // [10, 20, 30] (first three)
emit f"items[2:] = {items[2:]}"; // [30, 40, 50, 60, 70, 80]
emit f"items[-2:] = {items[-2:]}"; // [70, 80] (last two)
emit f"items[-3:-1] = {items[-3:-1]}"; // [60, 70]
}
Negative indices count from the end of the collection: -1 is the last element,
-2 is the second-to-last, and so on.
Slicing also works on strings:
{
let text = "Hello, World!";
emit f"text[:5] = {text[:5]}"; // Hello
emit f"text[7:] = {text[7:]}"; // World!
emit f"text[-6:] = {text[-6:]}"; // World!
}
Slicing always returns a new list (or string) -- it does not modify the original.
4.15 Type Conversion Functions #
You have already seen str() for converting values to strings. Neam provides
several conversion functions for moving between types:
| Function | Purpose | Example | Result |
|---|---|---|---|
str(x) |
Any value to string | str(42) |
"42" |
num(x) |
String to number | num("42") |
42 |
bool(x) |
Value to boolean | bool(0) |
true |
int(x) |
Truncate to integer | int(3.7) |
3 |
Unlike Python or JavaScript, bool(0) returns true in Neam because 0 is truthy.
Only false and nil are falsy. See Section 4.16 for the full truthiness rules.
{
// String to number
let input = "42";
let value = num(input);
emit str(value + 8); // 50
// Number to integer (truncation)
let pi = 3.14159;
emit str(int(pi)); // 3
// Parsing numeric strings
let price_str = "19.99";
let price = num(price_str);
let tax = price * 0.08;
// math_round() rounds to the nearest integer (covered in Chapter 5)
emit "Tax: $" + str(math_round(tax * 100) / 100);
}
num() will return nil if the string cannot be parsed as a number.
Always validate input before converting:
{
let input = "not a number";
let result = num(input);
if (result == nil) {
emit "Invalid number";
}
}
4.16 Truthiness and Falsiness #
In Neam, every value can be used in a Boolean context (like an if condition).
The rules are simple:
| Value | Truthiness |
|---|---|
false |
Falsy |
nil |
Falsy |
0 |
Truthy |
"" (empty string) |
Truthy |
| Everything else | Truthy |
Note that, unlike some languages, 0 and "" are truthy in Neam. Only false
and nil are falsy.
{
if (0) {
emit "Zero is truthy";
}
if (!nil) {
emit "Nil is falsy";
}
if ("") {
emit "Empty string is truthy";
}
}
Expected output:
Zero is truthy
Nil is falsy
Empty string is truthy
4.17 Working with nil #
nil appears frequently in Neam programs as a sentinel value meaning "no value"
or "not yet assigned." Understanding how to work with it safely is important.
{
let result = nil;
// Check for nil before using a value
if (result == nil) {
emit "No result available";
}
// nil converts to the string "nil"
emit "Value: " + str(result); // Value: nil
// nil is falsy
if (!result) {
emit "Result is nil or false";
}
}
A common pattern is to use nil as a default and then assign a real value later:
{
let winner = nil;
let score_a = 85;
let score_b = 92;
if (score_a > score_b) {
winner = "Team A";
}
if (score_b > score_a) {
winner = "Team B";
}
if (winner != nil) {
emit "Winner: " + winner;
}
}
Expected output:
Winner: Team B
4.18 Putting It All Together: A Practical Example #
Let us build a program that uses everything from this chapter. This program simulates a simple student grade calculator:
{
// Student data
let student_name = "Alice";
const MAX_SCORE = 100;
// Individual scores
let math_score = 92;
let science_score = 88;
let english_score = 95;
let history_score = 78;
// Calculate average
let total = math_score + science_score + english_score + history_score;
let num_subjects = 4;
let average = total / num_subjects;
// Display results
emit "=== Grade Report ===";
emit "Student: " + student_name;
emit "";
emit "Math: " + str(math_score) + " / " + str(MAX_SCORE);
emit "Science: " + str(science_score) + " / " + str(MAX_SCORE);
emit "English: " + str(english_score) + " / " + str(MAX_SCORE);
emit "History: " + str(history_score) + " / " + str(MAX_SCORE);
emit "";
emit "Total: " + str(total) + " / " + str(MAX_SCORE * num_subjects);
emit "Average: " + str(average);
// Determine grade using comparisons
let grade = "F";
if (average >= 90) {
grade = "A";
}
if ((average >= 80) && (average < 90)) {
grade = "B";
}
if ((average >= 70) && (average < 80)) {
grade = "C";
}
if ((average >= 60) && (average < 70)) {
grade = "D";
}
emit "Grade: " + grade;
// Additional analysis
let passed = average >= 60;
let honors = average >= 90;
emit "";
emit "Passed: " + str(passed);
emit "Honors: " + str(honors);
// Find highest and lowest
let highest = math_score;
let lowest = math_score;
if (science_score > highest) { highest = science_score; }
if (english_score > highest) { highest = english_score; }
if (history_score > highest) { highest = history_score; }
if (science_score < lowest) { lowest = science_score; }
if (english_score < lowest) { lowest = english_score; }
if (history_score < lowest) { lowest = history_score; }
emit "Highest: " + str(highest);
emit "Lowest: " + str(lowest);
emit "Range: " + str(highest - lowest);
}
Expected output:
=== Grade Report ===
Student: Alice
Math: 92 / 100
Science: 88 / 100
English: 95 / 100
History: 78 / 100
Total: 353 / 400
Average: 88.25
Grade: B
Passed: true
Honors: false
Highest: 95
Lowest: 78
Range: 17
What This Program Demonstrates #
| Concept | Where It Appears |
|---|---|
let variables |
student_name, math_score, etc. |
const |
MAX_SCORE |
| Number arithmetic | total / num_subjects, highest - lowest |
| String concatenation | "Student: " + student_name |
str() conversion |
str(math_score), str(average) |
| Comparison operators | average >= 90, score > highest |
| Logical operators | (average >= 80) && (average < 90) |
| Boolean values | passed, honors |
| Reassignment | grade = "A", highest = science_score |
4.19 Types in the Context of Agents #
Before closing this chapter, it is worth understanding how Neam's type system
connects to AI agent programming. Every concept you have learned here -- numbers,
strings, booleans, lists, maps, nil -- appears directly in agent development:
| Type | Agent Use Case |
|---|---|
| String | Agent responses, system prompts, user queries, tool descriptions |
| Number | Temperature settings, token limits, cost tracking, confidence scores |
| Boolean | Feature flags, guardrail pass/fail, comparison results |
| Nil | Absent tool results, optional parameters, uninitialized state |
| List | Multi-agent handoff targets, document chunks, batch responses |
| Map | Tool parameter schemas, agent configurations, structured LLM output |
| Tuple | Multi-return from tools (score, label), coordinate pairs, fixed records |
| Set | Deduplicating entities, unique tag collections, fast membership checks |
| Range | Batch iteration, paginated API calls, generating index sequences |
| Option | Safe tool return values, missing data handling, nullable fields |
| TypedArray | Embedding vectors, similarity scores, numeric feature arrays |
| Record | Structured tool results, parsed entities, domain objects |
| Table | API response tables, analytics data, CSV/JSON datasets |
For example, when you declare an agent in Chapter 10, the configuration is essentially a map:
agent Analyst {
provider: "openai" // String
model: "gpt-4o" // String
temperature: 0.3 // Number
}
When that agent calls a tool, the parameters are a map with typed values. When the
tool returns, you check for nil (no result) or parse the response string. When you
route between agents, you compare scores (numbers) and check conditions (booleans).
The foundational types you learned in this chapter are the building blocks of everything that follows.
4.20 Type Summary Reference #
| Type | Literal Examples | typeof() Result | Falsy? |
|---|---|---|---|
| Number | 42, 3.14, -7 |
"number" |
No |
| String | "hello", "", f"hi {x}" |
"string" |
No |
| Boolean | true, false |
"boolean" |
false only |
| Nil | nil |
"nil" |
Yes |
| List | [1, 2, 3], [] |
"list" |
No |
| Map | {"key": "val"}, {} |
"map" |
No |
| Tuple | (1, 2), ("a", 3) |
"tuple" |
No |
| Set | set(1, 2, 3) |
"set" |
No |
| Range | range(10), range(1, 5) |
"range" |
No |
| Option | Some(42), None |
"option" |
None only |
| TypedArray | float_array([1.0, 2.0]) |
"typed_array" |
No |
| Record | Point(10, 20) |
"record" |
No |
| Table | table({"col": [...]}) |
"table" |
No |
4.21 Chapter Summary #
In this chapter, you learned the data foundations of Neam:
letdeclares mutable variables;constdeclares immutable constants.- Variables are block-scoped -- visible from declaration to the end of the
enclosing
{ ... }block. Inner blocks can shadow outer variables. - Neam has thirteen core types: Number, String, Boolean, Nil, List, Map, Tuple, Set, Range, Option, TypedArray, Record, and Table.
- Type inference means you never write type annotations -- the language determines types from values automatically.
- Strings support concatenation (
+), length (len()), search (.contains()), transformation (.upper(),.lower()), and parsing (split(),replace()). - F-strings (
f"Hello, {name}!") provide concise string interpolation with embedded expressions. - Type coercion auto-converts
String + Numberto string concatenation,String * Numberto string repetition, andNumber + Boolto numeric addition. - Lists are ordered, zero-indexed collections with
append(),sort(),map(),filter(), and other functional operations. - Maps are key-value stores with string keys, supporting
keys(),values(),has_key(), and nested access -- directly mirroring JSON for LLM communication. - Tuples are immutable, fixed-size sequences accessed by position (
.0,.1) and supporting destructuring (let (x, y) = point;). - Sets hold unique elements with O(1) membership testing and set-algebra operations (union, intersection, difference).
- Ranges are lazy integer sequences (
range(n),range(start, end, step)) used primarily withforloops. - Options (
Some(value)/None) safely represent values that might not exist, with.unwrap(),.unwrap_or(), and.map()for safe access. - TypedArrays (
float_array(),int_array()) are homogeneous numeric arrays with vectorized operations (sum(),mean(),dot(),norm()). - Records (
record Point { x: number, y: number }) are immutable, hashable named tuples withto_map(),with(), andfrom_map(). - Tables (
table(data)) are columnar data structures withfilter(),sort_by(),group_by(),join(), and CSV/JSON export. - Arithmetic operators (
+,-,*,/,%) work on numbers. - The
+operator is overloaded: addition for numbers, concatenation for strings, and auto-coercion for mixed types. - Broadcasting applies arithmetic element-wise:
[1,2,3] + 10yields[11,12,13], and[1,2,3] + [4,5,6]yields[5,7,9]. - Comparison operators (
==,!=,<,>,<=,>=) return Booleans. - Logical operators (
&&,||,!) combine and negate Booleans with short-circuit evaluation. - The pipe operator (
|>) chains operations left-to-right for readable data pipelines. - The
inoperator tests membership in lists, sets, ranges, strings, and maps (key check). - Destructuring unpacks lists (
let [a, b, ...rest] = list;), tuples (let (x, y) = point;), and maps (let {name, age} = person;) into individual variables. - The spread operator (
...) merges lists ([...a, ...b]) and maps ({...defaults, ...overrides}). - Slicing (
items[2:5],items[::2],items[-2:]) extracts portions of lists and strings, with support for negative indices. - Type conversion functions include
str(),num(),bool(), andint(). typeof()inspects types at runtime.nilrepresents the absence of a value and is falsy.- Every type you learned here maps directly to agent programming concepts: strings for prompts, numbers for scores, maps for tool parameters, lists for handoff targets, options for safe tool returns, sets for deduplication, typed arrays for embeddings, records for structured results, and tables for datasets.
In the next chapter, you will learn how to organize your code into reusable functions -- the first step toward building larger, well-structured programs.
Exercises #
Exercise 4.1: Variable Swap
Declare two variables a and b with the values 10 and 20. Swap their values
(so a becomes 20 and b becomes 10) without hard-coding the numbers. Emit
both values before and after the swap. (Hint: you will need a temporary variable.)
Exercise 4.2: Temperature Converter
Write a program that converts a temperature from Fahrenheit to Celsius using the
formula: C = (F - 32) * 5 / 9. Store the Fahrenheit value in a variable, compute
the Celsius equivalent, and emit both values in a sentence like:
"72 degrees Fahrenheit is 22.2222 degrees Celsius".
Exercise 4.3: Type Detective
Write a program that creates one variable of each type (Number, String, Boolean,
Nil, List, Map) and uses typeof() to emit the type of each. Verify that the
output matches the table in Section 4.20.
Exercise 4.4: String Builder Given the following variables:
let city = "San Francisco";
let state = "CA";
let zip = 94102;
Write code that assembles and emits the string:
"San Francisco, CA 94102". Use concatenation and str().
Exercise 4.5: Comparison Table
Write a program that emits a table comparing two numbers. For example, given
let x = 15; and let y = 20;, emit:
x == y: false
x != y: true
x < y: true
x > y: false
x <= y: true
x >= y: false
Exercise 4.6: Boolean Logic
Write a program that defines three Boolean variables: has_ticket, is_vip, and
event_full. Determine whether a person can enter an event using these rules:
- A person can enter if they have a ticket AND the event is not full.
- A VIP can always enter, even if the event is full.
Emit the result for several combinations of values.
Exercise 4.7: Map Builder
Create a map representing a book with keys "title", "author", "year", and
"pages". Emit each field on its own line in a formatted display:
Title: The Pragmatic Programmer
Author: David Thomas & Andrew Hunt
Year: 2019
Pages: 352
Exercise 4.8: List Statistics
Create a list of five numbers. Using a for-in loop (preview of Chapter 6) and
variables, compute and emit the sum and the average of the numbers. You will need
len() and an accumulator variable.
Exercise 4.9: Nil Safety
Write a program where a variable starts as nil. Use an if statement to check
whether it is nil before attempting to use it. Then assign it a string value and
check again. Emit appropriate messages in each case.
Exercise 4.10: Calculator
Write a program that stores two numbers and an operator string (e.g., "+", "-",
"*", "/"). Use if statements to perform the correct operation based on the
operator string and emit the result. Handle division by zero by checking if the
second number is zero before dividing.
Exercise 4.11: Tuple Coordinates
Create a tuple representing a 2D point (3.0, 4.0). Use destructuring to extract
the x and y values into separate variables. Then compute the distance from the
origin using the formula sqrt(x*x + y*y) (you can use math_sqrt() or compute
it manually) and emit the result using an f-string:
"Distance from origin: 5".
Exercise 4.12: Set Deduplication Given two lists of names representing attendees from two events:
let event_a = ["Alice", "Bob", "Carol", "Dave"];
let event_b = ["Carol", "Dave", "Eve", "Frank"];
Use sets to find and emit: (a) all unique attendees across both events (union), (b) people who attended both events (intersection), and (c) people who attended only the first event (difference).
Exercise 4.13: Range Summation
Use range() and a for loop to compute the sum of all integers from 1 to 100
(inclusive). Emit the result. (The answer should be 5050.)
Exercise 4.14: Option Safety
Create a list and use .first() to get the first element. Do this for both an
empty list and a non-empty list. Use .unwrap_or() to provide a default value
when the list is empty. Emit the results using f-strings.
Exercise 4.15: Pipe Pipeline
Given the list [8, 3, 5, 1, 9, 2, 7, 4, 6, 3, 8, 1], use the pipe operator
to create a pipeline that: removes duplicates, sorts in ascending order, reverses
the order, and takes the top 5 elements. Emit the result.
Exercise 4.16: In Operator
Write a program that defines a list of allowed commands:
["help", "status", "run", "stop"]. Then check whether several test strings
are in the list and emit the results. Also check whether a substring is in
a longer string.
Exercise 4.17: Destructuring and Slicing
Given the list [10, 20, 30, 40, 50, 60, 70, 80, 90, 100]:
- Use destructuring with ...rest to extract the first two elements and the rest.
- Use slicing to extract elements at indices 3 through 6.
- Use slicing with a step of 3 to get every third element.
Emit all results using f-strings.
Exercise 4.18: F-String Formatter Rewrite the grade report from Section 4.18 using f-strings instead of string concatenation. Compare the readability of the two versions.
Exercise 4.19: TypedArray Statistics
Create a float_array with the values [4.0, 8.0, 15.0, 16.0, 23.0, 42.0].
Compute and emit the sum, mean, min, max, and standard deviation using
TypedArray methods. Then create a second array and compute their dot product.
Exercise 4.20: Broadcasting Math
Given the list [10, 20, 30, 40, 50], use broadcasting to:
- Add 5 to every element.
- Multiply every element by 3.
- Add it element-wise with [1, 2, 3, 4, 5].
Emit all three results using f-strings.
Exercise 4.21: Record Definition
Define a record Book { title: string, author: string, year: number }. Create
two book instances, emit their fields, convert one to a map with .to_map(),
and create one from a map with Book.from_map(). Use .with() to create a
new edition with an updated year.
Exercise 4.22: Table Manipulation
Create a table with columns "product", "price", and "quantity" containing
at least four rows. Then:
- Filter to keep only rows where price > 15.
- Sort by quantity in ascending order.
- Add a "total" column computed as price * quantity.
- Emit the final table using .to_string().
Exercise 4.23: Spread Operator Given two maps representing default and custom settings:
let defaults = {"color": "blue", "size": 12, "bold": false};
let custom = {"color": "red", "bold": true};
Use the spread operator to merge them (with custom overriding defaults) and
emit the result. Then use list spreading to combine [1, 2, 3] and [4, 5, 6]
with the value 0 inserted between them.
Exercise 4.24: Map Destructuring
Create a map representing a person with keys "name", "age", and "city".
Use map destructuring to extract the name and city into separate variables
and emit them using f-strings.