Programming Neam
📖 8 min read

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:

┌─────────────────────────────────────────────────────────────────────────────────┐
│                     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: {...}              }                                                 │
│  }                                                                              │
│                                                                                 │
└─────────────────────────────────────────────────────────────────────────────────┘
💠 Why This Matters for Agent Development

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 #

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

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

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

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

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

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

neam
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
}
📝 When to use `struct` vs `mut struct`
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.

neam
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)
}
Method Dispatch
Instance method:
p1.distance_to(p2)
└─ self = p1, other = p2
Static method:
Point.origin()
└─ no self, called on the type directly

Builder Pattern with Impl Blocks #

A common pattern is using impl to create a builder that constructs structs step by step:

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

neam
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
}
"Dog: Rex
(Husky)"

Multiple Traits on One Type #

A struct can implement multiple traits, giving it several interfaces:

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

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

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

neam
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
}
tag: 0
tag: 1
tag: 2

Real-World Example: HTTP Response Modeling #

Sealed types are perfect for modeling results that can succeed or fail in different ways:

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

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

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

neam
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
}
Match Expression Flow
match state {
Idle => "waiting" <-- checked first
Working(task) => f"busy: {task}" <-- extracts task
Done(result) => ...
Failed(error) => ...
}
The first matching arm executes.
Variables in patterns bind the variant's data.
💠 Why Sealed + Match Matters for Agents

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:

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

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

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

neam
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)     │                                          │
│  └──────────────────┘                                          │
└─────────────────────────────────────────────────────────────────┘
neam
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:

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

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

neam
pipeline ContentPipeline {
  steps: [research, draft, review, publish]
}

Dispatch #

Route tasks to specialized agents based on a classifier:

neam
dispatch TaskRouter {
  router: classifier,
  routes: {
    code: coder,
    text: writer,
    data: analyst,
  },
  fallback: general,
}

Parallel #

Execute multiple agents concurrently and merge results:

neam
parallel MultiSearch {
  agents: [web_search, doc_search, code_search],
  gather: merge_results,
}

Loop #

Iterate between a generator and critic until quality is met:

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

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

Task
(struct)
TaskRunner
(trait)
TaskResult
(sealed)
Priority
(sealed)
TaskLog
(mut struct)
TaskReport
(extend)
neam
// ── 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:


🔭 Looking Ahead: NeamClaw Traits

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:

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.

Start typing to search...