How do constrained types work in EK9?
← Advanced Type System · Ref: Q257
EK9 has two distinct constraint mechanisms with different semantics: value constraints that create disconnected types, and generic bounds that restrict type parameters.
VALUE CONSTRAINTS WITH CONSTRAIN AS
The 'constrain as' syntax creates a NEW type constrained by a pattern or condition:
Name as String constrain as matches /^[a-zA-Z -]+$/
Name is a new type that validates its content at construction time. A bare constructor ASSERTS validity: it panics at runtime on a set invalid value (and a literal constant that provably violates is the compile error E08260). Use the fallible '.of(value)' factory for untrusted input — it returns an unset value on failure instead of panicking. Name shares operators with String (you can compare, convert to string, etc.) but it is NOT a String subtype. You cannot pass a Name where a String is expected. This is a LIKE-A relationship, not an IS-A relationship.
A CONSTRAINT'S LITERAL MUST MATCH EXACTLY
The literal in a 'constrain as' is compared with the base type's operator exactly as written - it is never promoted. A Float constraint writes '>= 1.0', not '>= 1' (E08265), and a String constraint writes '<> "x"', not '<> 'x''. For a user-defined base you may instead add the operator that takes the literal's type. See Q1391.
RANGE CONSTRAINTS WITH CONSTRAIN
The 'constrain' syntax restricts numeric values to a range:
defines type PositiveIndex as Integer constrain > 0
PositiveIndex only accepts values greater than zero. Like 'constrain as', this creates a disconnected type.
GENERIC BOUNDS WITH CONSTRAIN BY
The 'constrain by' syntax restricts generic type parameters, as in 'describeAnimal() of type T constrain by Animal as open'.
This preserves IS-A relationships: T must be a subtype of the bound (here the user-defined class Animal; see Q195 for the complete compiling example). Inside the generic, you can call methods from the constraining type.
WHY DISCONNECTED TYPES?
Value-constrained types are deliberately disconnected from their base type. If EmailAddress extended String, you could pass an EmailAddress anywhere a String is expected, bypassing validation. By making them disconnected, the compiler prevents accidental substitution:
defines function processEmail() -> addr as EmailAddress <- rtn as Boolean: addr? // Only accepts validated emails processString() -> text as String <- rtn as Boolean: text? // EmailAddress cannot be passed here
This forces explicit conversion when crossing type boundaries.
CONSTRAINT VALIDATION
Constraints are checked at construction time. A bare constructor ASSERTS validity and panics at runtime if a set value violates the constraint (a violating literal constant is the compile error E08260). For untrusted input use the fallible '.of(value)' factory, which returns an unset value on failure instead of panicking:
badName <- Name().of("123") if badName? // Will not enter: constraint failed
Use the ? operator to check if construction succeeded.
See Q195 for generic type constraints with constrain by. See Q223 for constrained enumerations. See Q27 for static typing benefits. See Q269 for input validation with constrained types.
Example
defines module qa.advancedtypes.constrainedtypes defines class Person firstName as String? surname as String? default private Person() as pure Person() as pure -> first as String last as String firstName :=? String(first) surname :=? String(last) // Copy constructor: a constrained type (ValidPerson below) stores an INDEPENDENT copy of its // base, so the base needs a copy mechanism — either this public copy constructor Person(Person) // or a public no-arg constructor plus the ':=:' operator. Without one, ValidPerson is rejected // with E06131. Person keeps its no-arg constructor private, so the copy constructor is the fit. Person() as pure -> from as Person firstName :=? String(from.firstName) surname :=? String(from.surname) operator matches as pure -> pattern as RegEx <- rtn as Boolean: $this matches pattern operator $ as pure <- rtn as String: `${firstName} ${surname}` override operator ? as pure <- rtn as Boolean: firstName? and surname? defines type // Value constraint: Name must match letters and spaces only Name as String constrain as matches /^[a-zA-Z -]+$/ // Range constraint: PositiveIndex must be greater than zero PositiveIndex as Integer constrain > 0 //A Float constraint states its bound as a Float - 1.0, never 1 (E08265, see Q1391). Ratio as Float constrain as >= 1.0 // User-defined type constraint: Person must have first and last name ValidPerson as Person constrain as matches /^[a-zA-Z]+ [a-zA-Z]+$/ defines program ConstrainedTypesDemo() stdout <- Stdout() // === VALUE CONSTRAINT: Name === // Valid construction - fallible factory returns a set value goodName <- Name().of("Alice Smith") stdout.println(`Valid name set: ${goodName?}`) if goodName? stdout.println(`Name: ${goodName}`) // Invalid construction - fallible factory returns an unset value badName <- Name().of("123!@#") stdout.println(`Invalid name set: ${badName?}`) // === RANGE CONSTRAINT: PositiveIndex === // Valid: greater than zero goodIndex <- PositiveIndex().of(5) stdout.println(`Valid index set: ${goodIndex?}`) if goodIndex? stdout.println(`Index: ${goodIndex}`) // Invalid: zero is not > 0 - fallible factory returns unset badIndex <- PositiveIndex().of(0) stdout.println(`Zero index set: ${badIndex?}`) // Invalid: negative - fallible factory returns unset negIndex <- PositiveIndex().of(-3) stdout.println(`Negative index set: ${negIndex?}`) // === USER-DEFINED TYPE CONSTRAINT: ValidPerson === // Person with operator matches enables constraining goodPerson <- ValidPerson().of(Person("Alice", "Smith")) stdout.println(`Valid person set: ${goodPerson?}`) if goodPerson? stdout.println(`Person: ${goodPerson}`) // Person with numbers in name fails the regex constraint - fallible factory returns unset badPerson <- ValidPerson().of(Person("123", "456")) stdout.println(`Bad person set: ${badPerson?}`) // === ASSIGN-IF-UNSET WITH CONSTRAINTS === // Guard pattern works naturally with constrained types if validName <- Name().of("Bob Jones") stdout.println(`Guarded name: ${validName}`) if invalidName <- Name().of("") stdout.println("Should not print")
Common mistakes
E50060 — A constraint expression may only use operators that its base type provides. Person declares 'operator matches' but has no '>' operator, so the constraint '> 0' on ValidPerson cannot be resolved and the compiler reports E50060 on that line. Constrained types inherit operators from their base - a constraint that needs an operator the base does not have breaks. See ek9 -h E50060 for details.
Incorrect:
ValidPerson as Person constrain as > 0
Correct:
ValidPerson as Person constrain as matches /^[a-zA-Z]+ [a-zA-Z]+$/
E08265 — A constraint's literal must match the operator's parameter type exactly - it is never promoted. Float's '>=' takes a Float, so write 1.0. See Q1391.
Incorrect:
Ratio as Float constrain as >= 1
Correct:
Ratio as Float constrain as >= 1.0
Other ways to ask this
- What is the difference between constrain as and constrain by?
- How do I create a type with validation constraints?
- How do I restrict the values a type can hold?
- What is a constrained type in EK9?
Coming from another language?
Java: no built-in value constraints, uses Bean Validation annotations (@Pattern, @Min). Python: no type-level constraints, runtime checks with validators. Rust: no constrained types, uses newtype pattern with manual validation. Go: no type constraints, runtime validation. Kotlin: value classes with init validation, but still subtypes. Swift: no constrained types, uses property wrappers. C#: no type-level constraints, uses data annotations. EK9: built-in 'constrain as' for pattern constraints, 'constrain' for range constraints, creates disconnected types (LIKE-A not IS-A), validation at construction time: a bare constructor asserts validity (panics on a set invalid value, or compile error E08260 for a violating literal), while the fallible '.of(value)' factory returns unset on failure for untrusted input.
Keywords: type, type-system, constant, like-a, restrict, constraint, advanced, range, disconnected, validate, generic, pattern, constrain, validation, bound