Programming Neam
📖 25 min read

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:


4.1 Variable Declaration with let #

A variable is a named container for a value. In Neam, you create variables with the let keyword:

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

text
let <name> = <value>;

Key rules:

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

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

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

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

neam
{
  const LIMIT = 10;
  LIMIT = 20;  // ERROR: Cannot reassign constant 'LIMIT'
}

When to use const vs let:

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:

Neam Value Types
Number
(double)
String
"..."
Boolean
true/false
Nil
nil
List
[val, val, ...]
Ordered, mutable
Map
{"key": val}
Key-value pairs
Tuple
(val, val, ...)
Immutable, fixed
Set
Unique elements
No duplicates
Range
range(s, e)
Lazy sequence
Option
Some(v) / None
Safe nil handle
TypedArray
float_array()
Numeric, fast
Record
record Point {}
Named fields
Table
table(data)
Columnar data

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:

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

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

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

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

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

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

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

neam
{
  let result = nil;

  if (!result) {
    emit "No result yet";
  }
}

List #

Lists are ordered, indexed collections of values. They can hold values of different types:

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

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

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

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:

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

neam
{
  let config = {
    "host": "localhost",
    "port": 8080,
    "debug": true
  };

  emit "Server: " + config["host"] + ":" + str(config["port"]);
  emit "Debug mode: " + str(config["debug"]);
}

Expected output:

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

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

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

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

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

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

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

neam
{
  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}";
}
1 2
┌──
│ 3
3
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:

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

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

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

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

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

neam
{
  // 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` vs `None` — What's the Difference?

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.

neam
{
  // 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]
}
List vs TypedArray

Vectorized Operations #

TypedArrays support element-wise arithmetic without explicit loops. Operations apply to every element simultaneously:

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

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

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

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

neam
// 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
}
Record vs Map vs Tuple

Records are immutable -- you cannot change a field after creation. To get a modified copy, use the with() method:

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

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

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

neam
{
  let students = table({
    "name":  ["Alice", "Bob", "Carol", "Dave"],
    "score": [92, 85, 78, 95],
    "grade": ["A", "B", "C", "A"]
  });

  emit students.to_string();
}

Output:

name

Accessing Table Data #

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

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

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

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

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

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

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

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

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

  1. Unary negation (-x) -- highest
  2. Multiplication, division, modulo (*, /, %)
  3. Addition, subtraction (+, -) -- lowest

Use parentheses to override precedence when needed:

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

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

neam
{
  emit "ha" * 3;       // hahaha
  emit "-" * 20;       // --------------------
  emit "abc" * 0;      // (empty string)
}

Type Coercion Summary #

Automatic Type Coercion Rules

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

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


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

This means the right-hand side may not be evaluated at all:

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

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

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

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

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

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

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

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

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:

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

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

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

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

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

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

neam
{
  // Enumerate returns (index, value) tuples
  for item in ["alpha", "beta", "gamma"].enumerate() {
    emit f"  {item.0}: {item.1}";
  }
}

Expected output:

text
  0: alpha
  1: beta
  2: gamma
neam
{
  // 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:

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

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

neam
{
  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"}
}
Spread Operator Summary

The spread operator is especially useful for building agent configurations where you want to start with sensible defaults and selectively override:

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

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

Slicing also works on strings:

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

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.

neam
{
  // 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);
}
⚠️ Warning

num() will return nil if the string cannot be parsed as a number. Always validate input before converting:

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

neam
{
  if (0) {
    emit "Zero is truthy";
  }

  if (!nil) {
    emit "Nil is falsy";
  }

  if ("") {
    emit "Empty string is truthy";
  }
}

Expected output:

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

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

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

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

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

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

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

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:

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

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

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

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

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

Start typing to search...