Modeling Types
Two ways to make your domain types harder to misuse.
newtype
Section titled “newtype”newtype wraps a base type so a UserId isn’t interchangeable with a bare int,
even though it behaves like one at runtime. It stops the class of bug where you pass
an order id where a user id was expected.
from pyrula import newtype, newtype_strict
UserId = newtype("UserId", int)UserId(1) # 1, but typed as UserId
Email = newtype_strict("Email", str, validate=lambda s: "@" in s)Email("a@example.com") # okEmail("nope") # raises ValueErrornewtype_strict adds a validator. Pass err_msg to control the failure message.
Sealed unions
Section titled “Sealed unions”@sealed marks a closed set of cases. @case declares each variant as an immutable
dataclass. Together they give you a union you can pattern match over, with match
raising if you forget a case.
from pyrula import sealed, case, match, on
@sealedclass Shape: pass
@caseclass Circle(Shape): radius: float
@caseclass Rect(Shape): w: float h: float
def area(shape: Shape) -> float: return match(shape)( on(Circle, lambda c: 3.14159 * c.radius ** 2), on(Rect, lambda r: r.w * r.h), )
area(Circle(2.0)) # 12.566...match(value)(...) checks each on(Type, handler) in order and runs the first whose
type matches. No branch matches, it raises rather than returning None, so a missed
case fails loudly instead of slipping through.
Scala equivalent
Section titled “Scala equivalent”@sealed plus @case with match/on is Scala’s sealed trait with case class
variants and a match expression; an unmatched case raising mirrors a non-exhaustive
Scala match under -Xfatal-warnings. newtype fills the role of Scala 3 opaque types
(or value classes / tagged types) for a distinct type that erases to its base at runtime.