Skip to content

polymorphism

Traits, enums, and generics — Sema polymorphism without classes or inheritance.

Run it from sema/:

Terminal window
sema check examples/polymorphism
SEMA_STRICT=1 sema run examples/polymorphism
sema assure examples/polymorphism --grade silver
"""Polymorphism worked example (§3.9).
Demonstrates the whole trait trio and a functional pipeline in one runnable
program on the deterministic mock engine:
- a **struct conforming via the header list** (`Version`),
- a **struct conforming out of line** with `impl Ord for Money` (retrofit),
- an **enum conforming to the same trait** (`Severity`), defaults grafted too,
- a **bounded generic** `maximum[T: Ord]` reused across all three,
- **default methods** (`less`/`max2`/`clamp`) derived from one `compare`,
- a **supertrait** obligation (`Ord` requires `Eq`),
- **trait objects**: a heterogeneous `list[Renderer]` dispatched dynamically,
with `is` narrowing (open-world polymorphism — see `plugins.sema`),
- **functional style**: immutable bindings, a comprehension, conditional
expressions."""
assure gold
from polymorphism.order import Ord, maximum
from polymorphism.plugins import Renderer, Text, Bullet, Rule, render_all, count_rules
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)
# Out-of-line conformance: `Money` is declared plainly, then retrofitted to
# `Ord` — the same defaults graft on as if listed in the header.
struct Money:
minor_units: int
impl Ord for Money:
def compare(self, other: Money) -> int:
return self.minor_units - other.minor_units
# An enum conforms to the very same trait; `less`/`max2`/`clamp` graft onto it.
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()
def latest_version():
ensure result.major == 2
ensure result.minor == 0
return maximum([Version(major=1, minor=4), Version(major=2, minor=0), Version(major=1, minor=9)])
def maximum_money(prices: list[Money]):
require len(prices) > 0
return maximum(prices)
def highest_severity():
ensure result == Severity.high
return maximum([Severity.low, Severity.high, Severity.medium])
def clamped_version(value: Version, lower: Version, upper: Version) !{}:
return value.clamp(lower, upper)
def rendered_report():
ensure result == "Report|----|- alpha|- beta"
return render_all([Text(body="Report"), Rule(width=4), Bullet(item="alpha"), Bullet(item="beta")])
def rendered_rule_count(items: list[Renderer]):
return count_rules(items)
def default_methods_hold() !{}:
ensure result == true
lower = Version(major=1, minor=9)
upper = Version(major=2, minor=0)
return lower.less(upper) and upper.eq(Version(major=2, minor=0))
test "bounded generic selects latest Version 2.0":
latest = latest_version()
ensure latest.major == 2
ensure latest.minor == 0
test "out-of-line Money impl selects maximum 1799":
prices = [Money(minor_units=1299), Money(minor_units=999), Money(minor_units=1799)]
ensure maximum_money(prices).minor_units == 1799
test "bounded generic preserves a maximum in the first slot":
prices = [Money(minor_units=1799), Money(minor_units=1299), Money(minor_units=999)]
ensure maximum(prices).minor_units == 1799
test "enum Ord dispatch selects Severity.high":
ensure highest_severity() == Severity.high
test "enum rank distinguishes every variant":
ensure Severity.low.rank() == 0
ensure Severity.medium.rank() == 1
ensure Severity.high.rank() == 2
test "default clamp returns Version 2.0":
clamped = clamped_version(Version(major=5, minor=0), Version(major=1, minor=0), Version(major=2, minor=0))
ensure clamped.major == 2
ensure clamped.minor == 0
test "trait-object dispatch renders Report":
ensure rendered_report() == "Report|----|- alpha|- beta"
test "is narrowing counts one Rule":
items = [Text(body="Report"), Rule(width=4), Bullet(item="alpha"), Bullet(item="beta")]
ensure rendered_rule_count(items) == 1
test "Ord defaults provide less and eq":
ensure default_methods_hold() == true
def main() !{}:
ensure result == "latest=2.0 dearest=1799 cheaper=[1299, 999] top=high clamped=2.0 rendered=Report|----|- alpha|- beta rules=1"
# Bounded generic over a user struct.
latest = latest_version()
# Retrofitted struct + a functional comprehension using a default method.
prices = [Money(minor_units=1299), Money(minor_units=999), Money(minor_units=1799)]
dearest = maximum_money(prices)
cheaper = [p.minor_units for p in prices if p.less(dearest)]
# Enum ordering through the grafted defaults.
top = highest_severity()
# `clamp` is a default method that calls other defaults.
clamped = clamped_version(latest, latest, latest)
# Trait objects: heterogeneous lists dispatch dynamically inside the wrappers.
rendered = rendered_report()
rules = rendered_rule_count([Rule(width=len(rendered))])
summary = f"latest={latest.major}.{latest.minor} dearest={dearest.minor_units} cheaper={cheaper} top={top.name} clamped={clamped.major}.{clamped.minor} rendered={rendered} rules={rules}"
print(summary)
return summary
"""Algebraic conformances (§3.9): `std.algebra` traits implemented for real types.
Three conforming types, so the trait laws in `std.algebra` are exercised rather
than decorative:
- `IntSum` — the integer-addition monoid,
- `Text` — the string-concatenation monoid,
- `IntBox` — a one-slot container: the identity functor, and a monad.
`sema assure` instantiates every law of each trait — and of its supertraits —
against each of these types and property-tests it. A passing law means **no
counterexample was found in the generated cases**; it is not a proof.
## The failure mode, made visible
Add the type below and `sema assure` turns red. This is the output it actually
produces, copied from a run, not a paraphrase:
struct BadSum (Monoid):
value: int
def combine(self, other: BadSum) -> BadSum !{}:
return BadSum(value=self.value + other.value + 1)
def empty(self) -> BadSum !{}:
return BadSum(value=0)
properties: 2/4 held
✗ law Monoid.left_identity for BadSum: falsified — property is false at a = BadSum(value=0)
✗ law Monoid.right_identity for BadSum: falsified — property is false at a = BadSum(value=0)
✓ law Monoid.empty_is_canonical for BadSum: 64 cases
✓ law Semigroup.associativity for BadSum: 64 cases
[sema assure] FAIL (bronze)
Note what survives: `x + y + 1` *is* associative and `empty` *is* canonical, so
those two laws still hold. Only the two identity laws are refuted, which is
exactly where the defect is. `test "the broken monoid really is broken"` below
pins the same arithmetic, so this comment cannot rot into a lie without a test
going red."""
from std.algebra import Semigroup, Monoid, Functor, Monad, combine_all, identity
struct IntSum (Monoid):
"""Integers under addition, identity 0."""
value: int
def combine(self, other: IntSum) -> IntSum:
return IntSum(value=self.value + other.value)
def empty(self) -> IntSum:
return IntSum(value=0)
struct Text (Monoid):
"""Strings under concatenation, identity ""."""
body: str
def combine(self, other: Text) -> Text:
return Text(body=self.body + other.body)
def empty(self) -> Text:
return Text(body="")
struct IntBox (Monad):
"""One integer in a box: the identity functor, and a monad over it.
Endomorphic (`IntBox -> IntBox`) because Sema's erased generics have no
higher-kinded types — see the `std.algebra` module docstring."""
item: int
def map(self, f) -> IntBox !{}:
return IntBox(item=f(self.item))
def pure(self, value) -> IntBox:
return IntBox(item=value)
def bind(self, f) -> IntBox !{}:
return f(self.item)
# The Functor and Monad laws that quantify over arbitrary *functions* cannot be
# written as `law` clauses — a law quantifies its free identifiers over `Self`,
# and assurance generates no function values. They are witnessed here instead:
# concrete instances, honestly weaker than the universally quantified law.
test "functor composition holds for two concrete functions":
double = (n => n * 2)
succ = (n => n + 1)
box = IntBox(item=7)
ensure box.map(double).map(succ) == box.map((n => succ(double(n))))
test "monad left identity holds for a concrete function and value":
to_box = (n => IntBox(item=n * 3))
witness = IntBox(item=0)
ensure witness.pure(5).bind(to_box) == to_box(5)
test "monad associativity holds for two concrete functions":
f = (n => IntBox(item=n + 1))
g = (n => IntBox(item=n * 10))
box = IntBox(item=4)
ensure box.bind(f).bind(g) == box.bind((n => f(n).bind(g)))
test "identity is the functor's fixed point":
ensure IntBox(item=9).map(identity) == IntBox(item=9)
test "combine_all folds a semigroup":
ensure combine_all([IntSum(value=1), IntSum(value=2), IntSum(value=4)]) == IntSum(value=7)
ensure combine_all([Text(body="a"), Text(body="b")]) == Text(body="ab")
test "the broken monoid really is broken":
# `combine(x, y) = x + y + 1` is associative, but 0 is not its identity —
# exactly what the commented-out `BadSum` above would report.
combine = ((x, y) => x + y + 1)
ensure combine(combine(1, 2), 3) == combine(1, combine(2, 3))
ensure combine(0, 5) != 5
"""Reusable ordering vocabulary (§3.9).
`Ord` is built on the supertrait `Eq`. A conforming type supplies a single
required method — `compare` — and inherits every other operation as a *default
method*: `less`, `eq`, `max2`, and `clamp` are written once here and grafted
onto every type that conforms, with the type's own definition winning if it
provides one. `maximum` is a *bounded generic*: it works for any `T` that is
`Ord`, using only the trait's surface.
Both traits carry `law` clauses. A law is a universally quantified property over
the implementing type: every free identifier that is not a trait method and does
not resolve in this module (`a`, `b`, `c` below) ranges over `Self`, and a trait
method is callable receiver-first, so `compare(a, b)` means `a.compare(b)`.
`sema assure` generates values and looks for a counterexample — a law that
passes means none was found in the generated cases, not that the law is proved."""
pub trait Eq:
"""Equality by value."""
def eq(self, other: Self) -> bool !{}
law reflexive: eq(a, a)
law symmetric: eq(a, b) == eq(b, a)
pub trait Ord (Eq):
"""A total order. Supply `compare`; the rest is provided for free."""
def compare(self, other: Self) -> int !{}
# An `Ord` impl is held to `Eq`'s laws too, because `Eq` is a supertrait.
law compare_reflexive: compare(a, a) == 0
law antisymmetric: compare(a, b) == -compare(b, a)
law consistent_with_eq: eq(a, b) == (compare(a, b) == 0)
law transitive: (less(a, b) and less(b, c)) ⇒ less(a, c)
# --- default (provided) methods: written once, reused by every conformer ---
def less(self, other: Self) -> bool !{}:
return self.compare(other) < 0
def eq(self, other: Self) -> bool !{}:
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)
# Bounded generic (§3.9): `[T: Ord]` is erased at runtime, but the bound is the
# declared obligation that the element type provides `Ord`, so the body may use
# `max2`. Works uniformly for structs and enums that conform.
pub def maximum[T: Ord](xs: list[T]) -> T !{}:
require len(xs) > 0
mut best = xs[0]
for x in xs[1:]:
best = best.max2(x)
return best
"""Trait objects (§3.9) — open-world polymorphism.
A `Renderer` trait with several unrelated concrete implementations, held
together in one `list[Renderer]` and dispatched dynamically. This is the pattern
classes use *inheritance* for (a heterogeneous collection behind an interface),
done with traits + dynamic dispatch instead — third parties can add new
`Renderer`s without touching a central `enum`. `is` recovers the concrete type
when open-world code needs it."""
pub trait Renderer:
"""Anything that can render itself to a line of text."""
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
# A heterogeneous collection behind the trait, dispatched dynamically: each
# element is a different concrete type, resolved at the call site by its runtime
# type. No `enum`, no shared base class.
pub def render_all(items: list[Renderer]) -> str !{}:
lines = [it.render() for it in items]
return "|".join(lines)
# `is` narrowing: open-world code can still ask a value's concrete type or test
# trait conformance.
pub def count_rules(items: list[Renderer]) -> int !{}:
mut n = 0
for it in items:
n = n + (1 if it is Rule else 0)
return n

Algebraic conformances (§3.9): std.algebra traits implemented for real types.

Three conforming types, so the trait laws in std.algebra are exercised rather than decorative:

  • IntSum — the integer-addition monoid,
  • Text — the string-concatenation monoid,
  • IntBox — a one-slot container: the identity functor, and a monad.

sema assure instantiates every law of each trait — and of its supertraits — against each of these types and property-tests it. A passing law means no counterexample was found in the generated cases; it is not a proof.

Add the type below and sema assure turns red. This is the output it actually produces, copied from a run, not a paraphrase:

struct BadSum (Monoid):
value: int
def combine(self, other: BadSum) -&gt; BadSum !{}:
return BadSum(value=self.value + other.value + 1)
def empty(self) -&gt; BadSum !{}:
return BadSum(value=0)
properties: 2/4 held
✗ law Monoid.left_identity for BadSum: falsified — property is false at a = BadSum(value=0)
✗ law Monoid.right_identity for BadSum: falsified — property is false at a = BadSum(value=0)
✓ law Monoid.empty_is_canonical for BadSum: 64 cases
✓ law Semigroup.associativity for BadSum: 64 cases
[sema assure] FAIL (bronze)

Note what survives: x + y + 1 is associative and empty is canonical, so those two laws still hold. Only the two identity laws are refuted, which is exactly where the defect is. test "the broken monoid really is broken" below pins the same arithmetic, so this comment cannot rot into a lie without a test going red.

Integers under addition, identity 0.

Fields

field type descriptor
value int

Strings under concatenation, identity “”.

Fields

field type descriptor
body str

One integer in a box: the identity functor, and a monad over it.

Endomorphic (IntBox -> IntBox) because Sema’s erased generics have no higher-kinded types — see the std.algebra module docstring.

Fields

field type descriptor
item int

Polymorphism worked example (§3.9).

Demonstrates the whole trait trio and a functional pipeline in one runnable program on the deterministic mock engine:

  • a struct conforming via the header list (Version),
  • a struct conforming out of line with impl Ord for Money (retrofit),
  • an enum conforming to the same trait (Severity), defaults grafted too,
  • a bounded generic maximum[T: Ord] reused across all three,
  • default methods (less/max2/clamp) derived from one compare,
  • a supertrait obligation (Ord requires Eq),
  • trait objects: a heterogeneous list[Renderer] dispatched dynamically, with is narrowing (open-world polymorphism — see plugins.sema),
  • functional style: immutable bindings, a comprehension, conditional expressions.

Ordered by major, then minor.

Fields

field type descriptor
major int
minor int

Fields

field type descriptor
minor_units int

Variants

  • low
  • medium
  • high
def latest_version()
def maximum_money(prices: list[Money])

Parameters

name type
prices list[Money]
def highest_severity()
def clamped_version(value: Version, lower: Version, upper: Version) !{}

Parameters

name type
value Version
lower Version
upper Version

Effects !{}

def rendered_report()
def rendered_rule_count(items: list[Renderer])

Parameters

name type
items list[Renderer]
def default_methods_hold() !{}

Effects !{}

def main() !{}

Effects !{}

Reusable ordering vocabulary (§3.9).

Ord is built on the supertrait Eq. A conforming type supplies a single required method — compare — and inherits every other operation as a default method: less, eq, max2, and clamp are written once here and grafted onto every type that conforms, with the type’s own definition winning if it provides one. maximum is a bounded generic: it works for any T that is Ord, using only the trait’s surface.

Both traits carry law clauses. A law is a universally quantified property over the implementing type: every free identifier that is not a trait method and does not resolve in this module (a, b, c below) ranges over Self, and a trait method is callable receiver-first, so compare(a, b) means a.compare(b). sema assure generates values and looks for a counterexample — a law that passes means none was found in the generated cases, not that the law is proved.

Equality by value.

A total order. Supply compare; the rest is provided for free.

def maximum[T: Ord](xs: list[T]) -> T !{}

Parameters

name type
xs list[T]

Returns T

Effects !{}

Trait objects (§3.9) — open-world polymorphism.

A Renderer trait with several unrelated concrete implementations, held together in one list[Renderer] and dispatched dynamically. This is the pattern classes use inheritance for (a heterogeneous collection behind an interface), done with traits + dynamic dispatch instead — third parties can add new Renderers without touching a central enum. is recovers the concrete type when open-world code needs it.

Anything that can render itself to a line of text.

Fields

field type descriptor
body str

Fields

field type descriptor
item str

Fields

field type descriptor
width int
def render_all(items: list[Renderer]) -> str !{}

Parameters

name type
items list[Renderer]

Returns str

Effects !{}

def count_rules(items: list[Renderer]) -> int !{}

Parameters

name type
items list[Renderer]

Returns int

Effects !{}