Skip to content

Traits, Enums & Generics

Sema has no classes and no inheritance, by design. Everything class hierarchies are used for — shared behavior, polymorphism, heterogeneous collections — is done with traits (interfaces with default methods and laws) and algebraic data types (struct products, enum sums), Rust-style. This page is the core of what makes Sema’s object model different, and every snippet is drawn from the runnable polymorphism example.

Class inheritance conflates several distinct jobs (interface, code reuse, subtype polymorphism, open extension) into one mechanism, and the dynamic parts (metaclasses, monkey-patching, MRO surprises) break static compilation. Sema splits the jobs apart into orthogonal, checkable constructs:

Job Sema mechanism
Declare required behavior trait with bodyless defs
Reuse implementation trait default methods
Enforce algebraic laws trait law clauses (verified)
Open-world polymorphism trait objects (list[Renderer])
Closed-world exhaustiveness enum + match
Parametric reuse generics [T] with bounds

struct and enum bodies admit def — methods are ordinary functions with an implicit typed self. mut def marks methods that mutate self and is legal only through mut bindings. Associated constants are def-less bindings in the type body.

struct Money (Mergeable, Semantic):
currency: Currency
minor_units: i64
invariant minor_units >= 0
def combine(self, other: Money) -> Money !{}:
require self.currency == other.currency
return Money(currency=self.currency, minor_units=self.minor_units + other.minor_units)

Traits: required signatures, default methods, laws

Section titled “Traits: required signatures, default methods, laws”

A trait declares required signatures (bodyless defs), optional default methods (defs with a body), and laws as contracts (making trait obligations first-class verification targets, not documentation):

trait Mergeable:
sem "Types with an associative combine, safe for unordered parallel reduction"
def combine(self, other: Self) -> Self !{} # required
law associative: combine(combine(a, b), c) == combine(a, combine(b, c))

The law associative clause feeds the property engine — an unordered parallel reduce demands Mergeable with a killed-mutant record for associative, which is what “proved associative” concretely means. See /neurosymbolic/verification/.

Default (provided) methods — reuse without inheritance

Section titled “Default (provided) methods — reuse without inheritance”

A trait method that carries a body is a default: written once, grafted onto every conforming type that does not override it (the type’s own definition always wins). This is Sema’s answer to implementation reuse — the legitimate core of what inheritance is used for — without a class hierarchy. A single required method can seed an entire interface:

pub trait Eq:
"""Equality by value."""
def eq(self, other: Self) -> bool
pub trait Ord (Eq): # Eq is a *supertrait* of Ord
"""A total order. Supply `compare`; the rest is provided for free."""
def compare(self, other: Self) -> int # the one required method
# --- default methods: written once, reused by every conformer ---
def less(self, other: Self) -> bool:
return self.compare(other) < 0
def eq(self, other: Self) -> bool: # satisfies the Eq obligation
return self.compare(other) == 0
def max2(self, other: Self) -> Self:
return other if self.less(other) else self
def clamp(self, lo: Self, hi: Self) -> Self:
return lo if self.less(lo) else (hi if hi.less(self) else self)

trait Ord (Eq): declares that every Ord type is also an Eq type. The supertrait’s obligations flow down (a conformer must satisfy Eq’s required methods too, unless a default in the chain supplies them) and its defaults are available to Ord’s defaults. Supertrait sets meet transitively; cycles are rejected.

A type conforms in its header list, or out of line with impl Trait for Type:. The out-of-line form retrofits conformance onto existing (even prelude or FFI) types:

# 1. Header-list conformance — supply `compare`, inherit `less`/`eq`/`max2`/`clamp`.
struct Version (Ord):
"""Ordered by major, then minor."""
major: int
minor: int
def compare(self, other: Version) -> int:
return (self.major - other.major) if self.major != other.major else (self.minor - other.minor)
# 2. Out-of-line conformance — `Money` is declared plainly, then retrofitted.
struct Money:
minor_units: int
impl Ord for Money:
def compare(self, other: Money) -> int:
return self.minor_units - other.minor_units

Conformance is checked. A type that declares a user-defined trait but leaves a required method unimplemented — its own or a supertrait’s, and not covered by a default — is a hard sema check error naming the missing methods.

An enum is a sum type — a value is exactly one of its variants, and variants may carry payloads:

enum Escalation:
none
notify(channel: str)
page(oncall: str, deadline: Duration)
match esc:
case Escalation.page(oncall, deadline): dispatch(oncall, deadline)
case Escalation.notify(channel): post(channel)
case Escalation.none: pass

Variant payloads destructure positionally or by name in case patterns, and enum match is exhaustiveness-checked (see Pattern Matching). Enums can conform to traits and carry methods just like structs — the very same Ord trait grafts its defaults onto an enum:

enum Severity (Ord):
low
medium
high
def rank(self) -> int:
return 0 if self == Severity.low else (1 if self == Severity.medium else 2)
def compare(self, other: Severity) -> int:
return self.rank() - other.rank()

The inline form (sentiment: enum Sentiment: pos | neg | neutral) is sugar for a standalone payload-free declaration.

Type parameters are written [T, U] on def/struct/enum/impl. A parameter may name the traits it must satisfy — a bound — and multiple bounds join with + ([T: Eq + Ord]):

struct Box[T]:
value: T
# Bounded generic: works for any T that is Ord, using only the trait's surface.
pub def maximum[T: Ord](xs: list[T]) -> T !{}:
mut best = xs[0]
for x in xs:
best = best.max2(x) # `max2` is available because T is bounded by Ord
return best

One maximum works uniformly across a user struct, a retrofitted struct, and an enum:

latest = maximum([Version(major=1, minor=4), Version(major=2, minor=0)])
dearest = maximum([Money(minor_units=1299), Money(minor_units=999)])
top = maximum([Severity.low, Severity.high, Severity.medium])

Generics are erased at runtime — the interpreter is uniformly typed, so generics add expressiveness and documentation without a second type-checking regime. The bound is the declared obligation, surfaced to sema check, reflection, and docs. It is enforced at call sites where the argument’s concrete type is evident: passing a Blob that does not conform to Ord to maximum is a sema check error. Where the checker cannot resolve the type, the bound is left to runtime dispatch.

A trait name used in type positionx: Shape, list[Shape], -> Shape — is a trait object type: any value conforming to the trait, dispatched by its runtime type. This is how you get a heterogeneous collection behind one interface — the thing class hierarchies use inheritance for — and third parties can add new conformers without touching a central enum:

pub trait Renderer:
def render(self) -> str
pub struct Text (Renderer):
body: str
def render(self) -> str: return self.body
pub struct Bullet (Renderer):
item: str
def render(self) -> str: return f"- {self.item}"
pub struct Rule (Renderer):
width: int
def render(self) -> str: return "-" * self.width
# One list, three concrete types, dispatched dynamically at each call site.
pub def render_all(items: list[Renderer]) -> str !{}:
return "|".join([it.render() for it in items])

Method calls on a trait-object value resolve against the concrete runtime type — the same dispatch as x.method() everywhere. A call to a method the concrete type does not provide is a typed error at the call site. Trait-object slots are conformance-checked: a value whose concrete type is evident and does not conform (a Blob in a list[Shape] argument, a Shape-typed binding, or a -> Shape return) is a sema check error before the program runs.

The is test — narrowing for open-world code

Section titled “The is test — narrowing for open-world code”

value is Type and value is not Type return bool: true iff the value’s runtime type is that concrete type, or conforms to that trait (transitively through supertraits). Because value semantics make identity comparison meaningless, is is repurposed as the type/conformance test — the narrowing escape hatch when open-world code needs to recover a concrete type:

pub def count_rules(items: list[Renderer]) -> int !{}:
mut n = 0
for it in items:
n = n + (1 if it is Rule else 0) # `is` narrows to a concrete type
return n
if x is int: ... # works on built-in types too
You want… Use
A fixed set of cases, exhaustively handled enum + match
An open set — third parties add types later trait objects (list[Trait])

They are duals. Enums close the set and give you exhaustiveness; trait objects keep it open and give you extensibility.

Semantic (embedding + canonical flattening), Iterable/Iterator, Hashable, Eq/Ord, and Mergeable are the traits the language and stdlib build on. The Iterable trait is the single iteration protocol — for x in xs:, comprehensions, parallel, and stream consumption all take Iterable operands.

  • Missing required method (own or supertrait, no default) → sema check error naming the methods.
  • Non-conforming value in a trait-object slot (evident type) → sema check error.
  • Calling an unbounded generic’s method the bound doesn’t provide → the body cannot use it (bound is the obligation).
  • Two impls for the same (trait, type) → coherence error.
  • Supertrait cycle → rejected.