Chapter 9b: Object-Oriented Programming -- Structs, Traits, and Sealed Types #
"Types are the vocabulary of your domain. When the language lets you name and structure your concepts, the code explains itself."
Neam v0.7.1 introduced a complete object-oriented type system built around
structs, impl blocks, traits, sealed types, and match
expressions. Unlike traditional OOP languages that emphasize class hierarchies
and inheritance, Neam follows a composition-over-inheritance model inspired by
Rust and Swift: you define data with struct, attach behavior with impl,
share interfaces with trait, and model variants with sealed + match.
By the end of this chapter, you will be able to:
- Define immutable and mutable structs with typed fields
- Attach methods and static functions using
implblocks - Use positional and named construction
- Create modified copies with
withsyntax - Define and implement traits for shared interfaces
- Model state machines and variants with
sealedtypes - Pattern-match on sealed variants using
matchexpressions - Extend existing types with new methods using
extend - Use generics for type-parameterized structs
- Attach property observers (
willSet,didSet,guard) to mutable fields - Apply declarative agentic patterns (
pipeline,dispatch,parallel,loop)
┌─────────────────────────────────────────────────────────────────────────────────┐
│ NEAM TYPE SYSTEM OVERVIEW (v0.7.1) │
├─────────────────────────────────────────────────────────────────────────────────┤
│ │
│ DATA DEFINITIONS BEHAVIOR POLYMORPHISM │
│ ──────────────── ──────── ──────────── │
│ │
│ struct Point { impl Point { trait Describable { │
│ x: number, fn distance(self) {...} fn describe(self); │
│ y: number fn origin() {...} } │
│ } } │
│ impl Describable │
│ mut struct Counter { extend Point { for Point { │
│ value: number fn scale(self, f) {...} fn describe(self) │
│ } } {...} │
│ } │
│ sealed Shape { │
│ Circle(r: number), match shape { struct Pair<T, U> { │
│ Rect(w: number, Circle(r) => ..., first: T, │
│ h: number), Rect(w,h) => ..., second: U │
│ Point Point => ..., } │
│ } } │
│ │
│ AGENTIC PATTERNS OBSERVERS │
│ ──────────────── ───────── │
│ │
│ pipeline P { mut struct T { │
│ steps: [a, b, c] val: number { │
│ } willSet(n) {...} │
│ didSet {...} │
│ dispatch D { guard val >= 0; │
│ router: r, } │
│ routes: {...} } │
│ } │
│ │
└─────────────────────────────────────────────────────────────────────────────────┘
Agent systems are full of structured data: tasks with states, messages with
roles, configurations with typed fields. Without a type system, you model
everything with maps and strings -- leading to silent key-mismatch bugs at
runtime. Structs and sealed types turn those runtime errors into
compile-time guarantees. A sealed AgentState { Idle, Running(task), Error(msg) }
makes it impossible to forget a case in your state machine.
9b.1 Structs #
A struct defines a named type with typed fields. Structs are immutable by default -- once created, their fields cannot be changed.
Defining a Struct #
struct Point { x: number, y: number }
This declares a type called Point with two fields: x and y, both numbers.
Creating Instances #
Neam supports two construction styles:
struct Point { x: number, y: number }
{
// Positional construction -- fields assigned in declaration order
let p1 = Point(3, 4);
// Named construction -- fields assigned by name (any order)
let p2 = Point(y: 10, x: 5);
emit f"p1 = {p1}"; // Point(x: 3, y: 4)
emit f"p2 = {p2}"; // Point(x: 5, y: 10)
}
Accessing Fields #
Use dot notation to access fields:
struct Point { x: number, y: number }
{
let p = Point(3, 4);
emit f"x = {p.x}"; // x = 3
emit f"y = {p.y}"; // y = 4
}
Structural Equality #
Structs support structural equality -- two instances are equal if they have the same type and all fields are equal:
struct Point { x: number, y: number }
{
let a = Point(3, 4);
let b = Point(3, 4);
let c = Point(5, 6);
emit a == b; // true
emit a == c; // false
}
Copy-With: Creating Modified Copies #
Since structs are immutable, you cannot change a field directly. Instead, use
the with keyword to create a new instance with some fields changed:
struct Point { x: number, y: number }
{
let p1 = Point(3, 4);
let p2 = p1 with (x: 10);
emit f"p1 = {p1}"; // Point(x: 3, y: 4) -- unchanged
emit f"p2 = {p2}"; // Point(x: 10, y: 4) -- new instance
}
The with syntax copies all fields from the original and overrides only the
ones you specify. The original is never modified.
+-----------------------------------------------------------+
| Copy-With Flow |
| |
| p1 = Point(x: 3, y: 4) |
| | |
| | with (x: 10) |
| v |
| p2 = Point(x: 10, y: 4) p1 is unchanged |
+-----------------------------------------------------------+
Real-World Example: Agent Configuration #
Structs are ideal for modeling typed configurations in agent systems:
struct AgentConfig {
name: string,
provider: string,
model: string,
temperature: number,
max_tokens: number
}
struct ChatMessage {
role: string,
content: string,
timestamp: number
}
{
// Create a reusable configuration
let config = AgentConfig(
name: "Summarizer",
provider: "openai",
model: "gpt-4o-mini",
temperature: 0.3,
max_tokens: 1024
);
// Create different configs by copying and overriding
let creative_config = config with (
name: "StoryWriter",
temperature: 0.9,
max_tokens: 2048
);
emit f"Base: {config.name} (temp={config.temperature})";
// Base: Summarizer (temp=0.3)
emit f"Creative: {creative_config.name} (temp={creative_config.temperature})";
// Creative: StoryWriter (temp=0.9)
// Model a conversation history
let msg1 = ChatMessage("user", "Summarize this article", 1700000000);
let msg2 = ChatMessage("assistant", "Here is the summary...", 1700000001);
emit f"{msg1.role}: {msg1.content}";
// user: Summarize this article
}
┌─────────────────────────────────────────────────────────────────┐
│ Struct vs Mutable Struct │
│ │
│ struct Point { x, y } mut struct Counter { value } │
│ ┌─────────────────┐ ┌─────────────────────┐ │
│ │ x: 3 │ y: 4 │ │ value: 0 │ │
│ └─────────────────┘ └─────────────────────┘ │
│ │ │ │
│ │ with (x: 10) │ c.value = 5 │
│ v v │
│ ┌─────────────────┐ ┌─────────────────────┐ │
│ │ x: 10 │ y: 4 │ NEW copy │ value: 5 │ SAME │
│ └─────────────────┘ └─────────────────────┘ │
│ │
│ Immutable: original unchanged Mutable: modified in place │
└─────────────────────────────────────────────────────────────────┘
Mutable Structs #
When you need to modify fields in place, declare a struct with mut:
mut struct Counter { value: number }
impl Counter {
fn increment(self) {
self.value = self.value + 1;
return self;
}
fn get(self) {
return self.value;
}
}
{
let c = Counter(0);
c.value = 5;
emit f"counter = {c.value}"; // counter = 5
c.increment();
emit f"after increment = {c.value}"; // after increment = 6
}
| Use case | Choose |
|---|---|
| Data that should not change after creation | struct (default) |
| Accumulators, counters, caches | mut struct |
| Passing data between functions/agents | struct (safer) |
| In-place updates for performance | mut struct |
Prefer immutable struct unless you have a specific reason to mutate.
9b.2 Impl Blocks -- Adding Methods #
An impl block attaches methods to a struct. Methods that take self as the
first parameter are instance methods. Methods without self are static
methods.
struct Point { x: number, y: number }
impl Point {
// Instance method -- operates on a specific Point
fn distance_to(self, other) {
let dx = self.x - other.x;
let dy = self.y - other.y;
return dx * dx + dy * dy;
}
// Instance method -- returns a new Point
fn add(self, other) {
return Point(self.x + other.x, self.y + other.y);
}
// Static method -- no self parameter
fn origin() {
return Point(0, 0);
}
}
{
let p1 = Point(3, 4);
let p2 = Point(1, 1);
// Call instance methods with dot notation
emit f"distance squared = {p1.distance_to(p2)}"; // 13
// Method chaining
let p3 = p1.add(Point(1, 1));
emit f"p1 + (1,1) = {p3}"; // Point(x: 4, y: 5)
// Call static methods on the type name
let o = Point.origin();
emit f"origin = {o}"; // Point(x: 0, y: 0)
}
Builder Pattern with Impl Blocks #
A common pattern is using impl to create a builder that constructs
structs step by step:
struct HttpRequest {
method: string,
url: string,
headers: map,
body: string
}
impl HttpRequest {
// Static builder methods return new HttpRequest instances
fn get(url) {
return HttpRequest("GET", url, {}, "");
}
fn post(url) {
return HttpRequest("POST", url, {}, "");
}
// Instance methods return modified copies for chaining
fn with_header(self, key, value) {
let new_headers = self.headers;
new_headers[key] = value;
return self with (headers: new_headers);
}
fn with_body(self, body) {
return self with (body: body);
}
fn describe(self) {
return f"{self.method} {self.url} ({len(self.headers)} headers)";
}
}
{
// Fluent builder chain
let req = HttpRequest.post("https://api.example.com/agents")
.with_header("Content-Type", "application/json")
.with_header("Authorization", "Bearer sk-...")
.with_body('{"name": "MyAgent"}');
emit req.describe();
// POST https://api.example.com/agents (2 headers)
}
9b.3 Traits -- Shared Interfaces #
A trait defines a set of methods that types can implement. Traits are Neam's mechanism for polymorphism -- different types can share the same interface while providing their own implementations.
Defining and Implementing a Trait #
trait Describable {
fn describe(self);
}
struct Dog { name: string, breed: string }
impl Describable for Dog {
fn describe(self) {
return f"Dog: {self.name} ({self.breed})";
}
}
struct Car { make: string, model: string }
impl Describable for Car {
fn describe(self) {
return f"Car: {self.make} {self.model}";
}
}
{
let d = Dog("Rex", "Husky");
let c = Car("Toyota", "Camry");
emit d.describe(); // Dog: Rex (Husky)
emit c.describe(); // Car: Toyota Camry
}
Multiple Traits on One Type #
A struct can implement multiple traits, giving it several interfaces:
trait Serializable {
fn to_json(self);
}
trait Loggable {
fn log_line(self);
}
struct TaskResult {
task_id: string,
status: string,
output: string,
duration_ms: number
}
impl Serializable for TaskResult {
fn to_json(self) {
return f'\{"task_id":"{self.task_id}","status":"{self.status}","output":"{self.output}"\}';
}
}
impl Loggable for TaskResult {
fn log_line(self) {
return f"[{self.task_id}] {self.status} in {self.duration_ms}ms";
}
}
{
let result = TaskResult("t-42", "complete", "Summary generated", 1200);
emit result.to_json();
// {"task_id":"t-42","status":"complete","output":"Summary generated"}
emit result.log_line();
// [t-42] complete in 1200ms
}
Default Methods #
Traits can provide default implementations that types inherit automatically:
trait Loggable {
// Required -- each type must implement this
fn log_prefix(self);
// Default -- types get this for free (can override)
fn log(self, message) {
return f"[{self.log_prefix()}] {message}";
}
}
struct Service { name: string }
impl Loggable for Service {
fn log_prefix(self) {
return self.name;
}
// log() is inherited from the trait default
}
{
let svc = Service("auth");
emit svc.log("started"); // [auth] started
}
9b.4 Sealed Types -- Algebraic Data Types #
A sealed type defines a fixed set of variants. Each variant can carry different data. Sealed types are Neam's version of algebraic data types (also called enums with data, sum types, or discriminated unions in other languages).
Defining a Sealed Type #
sealed Shape {
Circle(radius: number),
Rectangle(width: number, height: number),
Point
}
This declares three variants:
- Circle carries a radius
- Rectangle carries width and height
- Point carries no data (a unit variant)
Creating Variants #
sealed Shape {
Circle(radius: number),
Rectangle(width: number, height: number),
Point
}
{
let c = Shape.Circle(5);
let r = Shape.Rectangle(3, 4);
let p = Shape.Point;
emit f"c = {c}"; // Circle(radius: 5)
emit f"r = {r}"; // Rectangle(width: 3, height: 4)
emit f"p = {p}"; // Point
}
Real-World Example: HTTP Response Modeling #
Sealed types are perfect for modeling results that can succeed or fail in different ways:
sealed HttpResponse {
Success(status: number, body: string),
ClientError(status: number, message: string),
ServerError(status: number, retry_after: number),
Timeout
}
{
let responses = [
HttpResponse.Success(200, '{"agents": [...]}'),
HttpResponse.ClientError(401, "Invalid API key"),
HttpResponse.ServerError(503, 30),
HttpResponse.Timeout,
];
for (resp in responses) {
let description = match resp {
Success(status, body) => f"OK ({status}): {body}",
ClientError(status, msg) => f"Client error {status}: {msg}",
ServerError(status, retry) => f"Server error {status}, retry in {retry}s",
Timeout => "Request timed out",
};
emit description;
}
// OK (200): {"agents": [...]}
// Client error 401: Invalid API key
// Server error 503, retry in 30s
// Request timed out
}
Impl Blocks on Sealed Types #
You can attach methods to sealed types just like structs. Use match inside
methods to handle each variant:
sealed Shape {
Circle(radius: number),
Rectangle(width: number, height: number),
Point
}
impl Shape {
fn area(self) {
return match self {
Circle(radius) => 3.14159 * radius * radius,
Rectangle(width, height) => width * height,
Point => 0,
};
}
fn name(self) {
return match self {
Circle(radius) => "circle",
Rectangle(width, height) => "rectangle",
Point => "point",
};
}
}
{
let c = Shape.Circle(10);
let r = Shape.Rectangle(5, 3);
emit f"circle area = {c.area()}"; // 314.159
emit f"rect area = {r.area()}"; // 15
emit f"shape name = {c.name()}"; // circle
}
9b.5 Match Expressions -- Pattern Matching #
The match expression branches on the variant of a sealed type. It is an
expression -- it returns a value.
sealed AgentState {
Idle,
Working(task: string),
Done(result: string),
Failed(error: string)
}
{
let state = AgentState.Working("summarize");
let status = match state {
Idle => "waiting for work",
Working(task) => f"busy: {task}",
Done(result) => f"finished: {result}",
Failed(error) => f"error: {error}",
};
emit f"Agent status: {status}"; // Agent status: busy: summarize
}
Wildcard Matching #
Use _ to match any remaining variants:
sealed Shape {
Circle(radius: number),
Rectangle(width: number, height: number),
Point
}
{
let s = Shape.Circle(5);
let is_circle = match s {
Circle(r) => true,
_ => false,
};
emit f"is circle? {is_circle}"; // is circle? true
}
Agent state machines are a natural fit for sealed types. Instead of using
strings to represent states ("idle", "running", "error"), you define
a sealed AgentState with typed variants. The match expression forces
you to handle every case -- if you add a new state later and forget to
handle it, the compiler warns you.
9b.6 Extend -- Adding Methods After Definition #
The extend keyword lets you add new methods to an existing type without
modifying its original definition. This is useful for adding utility methods
or adapting types to new traits:
struct Point { x: number, y: number }
impl Point {
fn magnitude(self) {
return self.x * self.x + self.y * self.y;
}
}
// Later, in another part of the codebase:
extend Point {
fn to_string(self) {
return f"({self.x}, {self.y})";
}
fn scale(self, factor) {
return Point(self.x * factor, self.y * factor);
}
fn negate(self) {
return Point(0 - self.x, 0 - self.y);
}
}
{
let p = Point(3, 4);
emit p.to_string(); // (3, 4)
emit p.scale(2).to_string(); // (6, 8)
emit p.negate().to_string(); // (-3, -4)
emit f"magnitude = {p.magnitude()}"; // magnitude = 25
}
You can also extend sealed types and add static methods:
extend Point {
fn unit_x() {
return Point(1, 0);
}
}
{
let ux = Point.unit_x();
emit f"unit_x = ({ux.x}, {ux.y})"; // unit_x = (1, 0)
}
9b.7 Generics #
Structs can accept type parameters using angle bracket syntax. Neam generics are type-erased at runtime (similar to Go or TypeScript) -- the type parameters serve as documentation and IDE support:
struct Pair<T, U> { first: T, second: U }
{
let p = Pair("hello", 42);
emit f"first = {p.first}"; // first = hello
emit f"second = {p.second}"; // second = 42
}
struct Wrapper<T> { inner: T }
{
let w = Wrapper("payload");
emit f"inner = {w.inner}"; // inner = payload
}
Generics with Impl Blocks #
You can attach methods to generic structs:
struct Result<T, E> { ok: bool, value: T, error: E }
impl Result {
fn is_ok(self) {
return self.ok;
}
fn unwrap(self) {
if (self.ok) {
return self.value;
}
return nil;
}
fn unwrap_or(self, default) {
if (self.ok) {
return self.value;
}
return default;
}
fn map(self, transform) {
if (self.ok) {
return Result(true, transform(self.value), nil);
}
return self;
}
}
{
let success = Result(true, "Agent completed task", nil);
let failure = Result(false, nil, "Connection timeout");
emit f"success? {success.is_ok()}"; // success? true
emit f"value: {success.unwrap()}"; // value: Agent completed task
emit f"failure fallback: {failure.unwrap_or('no result')}";
// failure fallback: no result
// Transform the success value
let upper = success.map(fn(v) { return uppercase(v); });
emit f"mapped: {upper.unwrap()}";
// mapped: AGENT COMPLETED TASK
}
┌─────────────────────────────────────────────────────────────────┐
│ Generic Type Erasure │
│ │
│ Source code: VM runtime: │
│ │
│ struct Pair<T, U> { struct Pair { │
│ first: T, ──> first: any, │
│ second: U second: any │
│ } } │
│ │
│ Pair("hello", 42) Pair("hello", 42) │
│ │
│ Type parameters guide your thinking; the VM stores any value. │
└─────────────────────────────────────────────────────────────────┘
Generics are especially useful for reusable container types and for documenting the expected types in function signatures.
9b.8 Property Observers #
Mutable structs can attach property observers to fields. These are callbacks that fire when a field's value changes:
| Observer | When it fires | Access |
|---|---|---|
willSet(newVal) |
Before the new value is stored | Receives the incoming value |
didSet |
After the new value is stored | Can read the current value |
guard <expr> |
Before the new value is stored | Rejects the change if the expression is false |
┌─────────────────────────────────────────────────────────────────┐
│ Property Observer Execution Flow │
│ │
│ t.value = 30 │
│ │ │
│ v │
│ ┌──────────────────┐ │
│ │ guard expression? │──── false ──── REJECT (value unchanged) │
│ └────────┬─────────┘ │
│ │ true (or no guard) │
│ v │
│ ┌──────────────────┐ │
│ │ willSet(newVal) │──── runs BEFORE store │
│ │ (receives 30) │ │
│ └────────┬─────────┘ │
│ │ │
│ v │
│ ┌──────────────────┐ │
│ │ STORE new value │──── value = 30 │
│ └────────┬─────────┘ │
│ │ │
│ v │
│ ┌──────────────────┐ │
│ │ didSet │──── runs AFTER store │
│ │ (can read 30) │ │
│ └──────────────────┘ │
└─────────────────────────────────────────────────────────────────┘
mut struct Temperature {
value: number {
willSet(newVal) {
emit f"Temperature changing to {newVal}";
}
didSet {
emit f"Temperature updated";
}
}
}
{
let t = Temperature(25);
emit f"initial = {t.value}"; // initial = 25
t.value = 30;
// Output:
// Temperature changing to 30
// Temperature updated
emit f"final = {t.value}"; // final = 30
}
Guard Expressions #
A guard prevents invalid values from being assigned:
mut struct PositiveCounter {
count: number {
guard count >= 0;
}
}
{
let pc = PositiveCounter(5);
pc.count = 10;
emit f"count = {pc.count}"; // count = 10
// pc.count = -1; // Would be rejected by the guard
}
Practical Example: Token Budget Tracker #
Property observers are especially useful for agent systems that need to track resource consumption with built-in constraints:
mut struct TokenBudget {
remaining: number {
guard remaining >= 0;
willSet(newVal) {
emit f"[budget] tokens: {remaining} -> {newVal}";
}
didSet {
if (remaining < 100) {
emit "[budget] WARNING: less than 100 tokens remaining!";
}
}
}
}
{
let budget = TokenBudget(1000);
// Simulate agent calls consuming tokens
budget.remaining = budget.remaining - 350;
// [budget] tokens: 1000 -> 650
budget.remaining = budget.remaining - 500;
// [budget] tokens: 650 -> 150
budget.remaining = budget.remaining - 100;
// [budget] tokens: 150 -> 50
// [budget] WARNING: less than 100 tokens remaining!
emit f"Final budget: {budget.remaining}"; // Final budget: 50
// budget.remaining = -10; // REJECTED by guard (remaining >= 0)
}
9b.9 Declarative Agentic Patterns #
Neam v0.7.1 introduced four declarative patterns for common multi-agent architectures. These are defined at the top level and compile into configuration that the runner uses at execution time:
Pipeline #
A sequential chain of processing steps:
pipeline ContentPipeline {
steps: [research, draft, review, publish]
}
Dispatch #
Route tasks to specialized agents based on a classifier:
dispatch TaskRouter {
router: classifier,
routes: {
code: coder,
text: writer,
data: analyst,
},
fallback: general,
}
Parallel #
Execute multiple agents concurrently and merge results:
parallel MultiSearch {
agents: [web_search, doc_search, code_search],
gather: merge_results,
}
Loop #
Iterate between a generator and critic until quality is met:
loop RefineLoop {
generator: drafter,
critic: reviewer,
max_iterations: 3,
}
+-----------------------------------------------------------+
| Declarative Agentic Patterns |
| |
| pipeline A ──> B ──> C ──> D (sequential) |
| |
| dispatch ┌── A |
| R ───┤── B (fan-out by type) |
| └── C |
| |
| parallel ┌── A ──┐ |
| ├── B ──┤── merge (concurrent) |
| └── C ──┘ |
| |
| loop G ──> C ──> G ──> C ... (refine until done) |
+-----------------------------------------------------------+
These patterns compose with structs and sealed types for type-safe agent orchestration. For example, you can track pipeline stage progression using a sealed type:
sealed PipelineStage {
Pending(task: string),
InProgress(task: string, step: string),
Complete(result: string)
}
{
let stage = PipelineStage.InProgress("summarize", "review");
let status = match stage {
Pending(task) => f"waiting: {task}",
InProgress(task, step) => f"doing {step} on {task}",
Complete(result) => f"done: {result}",
};
emit f"Pipeline: {status}";
// Pipeline: doing review on summarize
}
9b.10 Struct vs Record #
Chapter 7 introduced the Record type for lightweight named data. Here is
how it compares to struct:
| Feature | Record | Struct |
|---|---|---|
| Mutability | Always immutable | Immutable by default, mut optional |
| Methods | No impl blocks |
Full impl + extend support |
| Traits | Cannot implement traits | Can implement traits |
| Generics | No | Yes |
| Property observers | No | Yes (on mut struct) |
| Pattern matching | No | Yes (via sealed) |
| Use case | Quick data containers | Domain models with behavior |
Use Record when you just need a named tuple. Use struct when you need
methods, traits, or the full type system.
9b.11 Putting It All Together: An Agent Task System #
This section combines structs, traits, sealed types, match, impl, extend, and generics to model a complete agent task management system. This is the kind of code you would write in a real Neam project.
// ── Data Model: structs and sealed types ──
struct Task {
id: string,
description: string,
priority: number,
assigned_to: string
}
sealed TaskResult {
Success(output: string, tokens_used: number),
Failure(error: string, retryable: bool),
Timeout
}
sealed Priority {
Critical,
High,
Normal,
Low
}
// ── Behavior: impl blocks ──
impl Task {
fn summary(self) {
return f"[{self.id}] {self.description} (assigned: {self.assigned_to})";
}
fn with_agent(self, agent_name) {
return self with (assigned_to: agent_name);
}
}
impl TaskResult {
fn is_success(self) {
return match self {
Success(output, tokens) => true,
_ => false,
};
}
fn describe(self) {
return match self {
Success(output, tokens) => f"OK ({tokens} tokens): {output}",
Failure(error, retryable) => f"FAIL: {error}" +
(retryable ? " (retryable)" : ""),
Timeout => "TIMEOUT: no response within deadline",
};
}
}
impl Priority {
fn weight(self) {
return match self {
Critical => 100,
High => 75,
Normal => 50,
Low => 25,
};
}
fn label(self) {
return match self {
Critical => "CRITICAL",
High => "HIGH",
Normal => "NORMAL",
Low => "LOW",
};
}
}
// ── Interface: traits ──
trait TaskRunner {
fn run_task(self, task);
fn name(self);
// Default method
fn log_start(self, task) {
return f"[{self.name()}] Starting: {task.summary()}";
}
}
struct LLMRunner { provider: string, model: string }
struct ToolRunner { skill_name: string }
impl TaskRunner for LLMRunner {
fn run_task(self, task) {
// In a real system, this would call the LLM
return TaskResult.Success(
f"Generated by {self.model}",
512
);
}
fn name(self) {
return f"LLM({self.provider}/{self.model})";
}
}
impl TaskRunner for ToolRunner {
fn run_task(self, task) {
return TaskResult.Success(
f"Executed skill: {self.skill_name}",
0
);
}
fn name(self) {
return f"Tool({self.skill_name})";
}
}
// ── Extension: add reporting methods later ──
extend TaskResult {
fn to_log_entry(self, task_id) {
let status = match self {
Success(output, tokens) => "SUCCESS",
Failure(error, retryable) => "FAILURE",
Timeout => "TIMEOUT",
};
return f"[{task_id}] {status}: {self.describe()}";
}
}
// ── Mutable state: tracking execution ──
mut struct TaskLog {
entries: list,
total_tokens: number
}
impl TaskLog {
fn new() {
return TaskLog([], 0);
}
fn record(self, task_id, result) {
push(self.entries, result.to_log_entry(task_id));
// Track token usage from successful results
let tokens = match result {
Success(output, tokens_used) => tokens_used,
_ => 0,
};
self.total_tokens = self.total_tokens + tokens;
}
fn summary(self) {
return f"{len(self.entries)} tasks, {self.total_tokens} tokens used";
}
}
// ── Main: wire it all together ──
{
// Create tasks
let tasks = [
Task("t-1", "Summarize quarterly report", 1, ""),
Task("t-2", "Look up stock price", 2, ""),
Task("t-3", "Draft email to client", 3, ""),
];
// Create runners
let llm = LLMRunner("openai", "gpt-4o-mini");
let tool = ToolRunner("stock_lookup");
// Track results
let log = TaskLog.new();
// Assign and run tasks
let t1 = tasks[0].with_agent("Summarizer");
emit llm.log_start(t1);
// [LLM(openai/gpt-4o-mini)] Starting: [t-1] Summarize quarterly report (assigned: Summarizer)
let r1 = llm.run_task(t1);
log.record(t1.id, r1);
emit r1.describe();
// OK (512 tokens): Generated by gpt-4o-mini
let t2 = tasks[1].with_agent("StockBot");
let r2 = tool.run_task(t2);
log.record(t2.id, r2);
emit r2.describe();
// OK (0 tokens): Executed skill: stock_lookup
// Simulate a failure
let r3 = TaskResult.Failure("Model overloaded", true);
log.record("t-3", r3);
emit r3.describe();
// FAIL: Model overloaded (retryable)
// Print execution summary
emit f"\n=== Execution Log ===";
for (entry in log.entries) {
emit entry;
}
emit f"Summary: {log.summary()}";
// Summary: 3 tasks, 512 tokens used
}
This example demonstrates how Neam's OOP features compose naturally:
struct Taskmodels immutable task data withwithfor reassignmentsealed TaskResultandsealed Priorityencode all possible outcomes and priority levels as type-safe variantstrait TaskRunnerdefines a common interface with a default logging methodimplblocks attach behavior to each typeextendadds ato_log_entry()method toTaskResultwithout touching its original definitionmut struct TaskLogtracks mutable execution statematchexpressions handle every variant exhaustively
NeamClaw Traits
The trait system you learned in this chapter is the foundation for NeamClaw's six
built-in agent traits: Schedulable (heartbeat and cron scheduling),
Channelable (multi-channel message routing), Sandboxable (per-execution
isolation), Monitorable (behavioral anomaly detection), Orchestrable
(multi-agent spawn/delegate callbacks), and Searchable (RAG/search
configuration). Each trait is implemented using the same impl Trait for Type
syntax you have already practiced. NeamClaw also introduces four built-in sealed
types -- HeartbeatAction, AnomalyAction, VerifyResult, and LoopOutcome --
that use match expressions just like the sealed types in this chapter. You will
explore all six traits in Chapter 27 and the sealed types in Chapters 24--25.
9b.12 Chapter Summary #
Neam's OOP system provides a modern, composition-based approach to structured programming:
structdefines named types with typed fields. Immutable by default.mut structallows field mutation after creation.implattaches instance methods (selfparameter) and static methods.- Positional (
Point(3, 4)) and named (Point(x: 3, y: 4)) construction. withcreates modified copies of immutable structs.- Structural equality compares fields, not identity.
traitdefines shared interfaces with required and default methods.impl Trait for Typeimplements a trait for a struct or sealed type.sealeddefines closed variant types (algebraic data types).matchpattern-matches on sealed variants, extracting data from each arm.extendadds new methods to existing types retroactively.- Generics (
struct Pair<T, U>) provide type-parameterized structs (type-erased at runtime). - Property observers (
willSet,didSet,guard) attach callbacks and constraints to mutable struct fields. - Declarative patterns (
pipeline,dispatch,parallel,loop) provide high-level agent orchestration primitives.
These features work together to let you model agent state machines, typed configurations, and structured data with compile-time safety.
Exercises #
Exercise 9b.1: RGB Color
Define a struct Color { r: number, g: number, b: number }. Add an impl
block with a to_hex(self) method that returns a string like "#FF8000".
Add a static method white() that returns Color(255, 255, 255). Test both.
Exercise 9b.2: Trait Practice
Define a trait Printable { fn display(self); }. Create two structs --
Book { title: string, author: string } and Movie { title: string, year: number }.
Implement Printable for both. Create instances and call .display() on each.
Exercise 9b.3: Agent State Machine
Define a sealed RequestState { Pending, Processing(agent: string), Complete(response: string), Failed(error: string) }.
Write a function describe_state(state) that uses match to return a
human-readable description of each state. Test with all four variants.
Exercise 9b.4: Extend and Chain
Define a struct Vec3 { x: number, y: number, z: number } with basic methods.
Then use extend to add length(self) and normalize(self) methods. Chain
them to normalize a vector and emit the result.
Exercise 9b.5: Property Observer
Create a mut struct Budget { remaining: number } with a guard remaining >= 0
and a willSet that logs changes. Test by setting the budget to various values.
Exercise 9b.6: Generic Stack
Implement a mut struct Stack<T> { items: list } with methods push(self, item),
pop(self), peek(self), and is_empty(self). Test it with both strings and
numbers. Then implement a trait Countable { fn count(self); } for your Stack
and verify it returns the correct count.
Exercise 9b.7: Sealed + Match Calculator
Define a sealed Operation { Add(a: number, b: number), Sub(a: number, b: number),
Mul(a: number, b: number), Div(a: number, b: number) }. Write a function
evaluate(op) that uses match to compute the result, returning
TaskResult.Success(result) for valid operations and TaskResult.Failure("division
by zero") for Div(_, 0).
Exercise 9b.8: Multi-Trait Agent
Create a struct SmartAgent { name: string, model: string } that implements three
traits: Describable (returns a description), Serializable (returns JSON), and
Comparable (compares by name). Create two agents and demonstrate all three trait
methods.