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.
Why no classes?
Section titled “Why no classes?”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 |
Methods on structs and enums
Section titled “Methods on structs and enums”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)Supertraits
Section titled “Supertraits”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.
Conforming to a trait — two forms
Section titled “Conforming to a trait — two forms”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_unitsConformance 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.
Enums: sum types with payloads
Section titled “Enums: sum types with payloads”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: passVariant 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.
Generics and bounds
Section titled “Generics and bounds”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 bestOne 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.
Trait objects — open-world polymorphism
Section titled “Trait objects — open-world polymorphism”A trait name used in type position — x: 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 tooOpen-world vs closed-world: which to use
Section titled “Open-world vs closed-world: which to use”| 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.
Core prelude traits
Section titled “Core prelude traits”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.
Failure modes
Section titled “Failure modes”- Missing required method (own or supertrait, no default) →
sema checkerror naming the methods. - Non-conforming value in a trait-object slot (evident type) →
sema checkerror. - 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.
See also
Section titled “See also”- Pattern Matching — how to destructure enums and structs.
- Types — the scalar and collection types traits build on.
polymorphismexample — the full runnable source.- Semantic values — how the
Semantictrait works.