Distinct types
A distinct type is an ordinary single-field type, and `by` forwards specific traits to that field so the wrapper costs no boilerplate - there is no separate opaque-alias feature.
There is no separate concept for "a type that wraps another type but is not interchangeable with it" - that is a
type with one field. by forwards specific traits to that field, one decision per trait, so the wrapper costs
nothing to use.
Example#
use Add, Compare from "std/core"
type Seconds with Show, Add by value, Compare by value {
value: Int
}
const total = Seconds(5) + Seconds(2)
print total
const raw = total.value
print rawSyntax#
type Name with Trait, (Trait, Trait) by field, Trait by field { ... }Rules#
A distinct type is a
typewith a single field, unlike a type alias, which is interchangeable with its target.SecondsandIntabove are never interchangeable:Seconds(5) + 2does not type check.Does not compiletype Seconds with Add by value { value: Int } const total = Seconds(5) + 2 // error: Expected `Seconds`, found `Int64`bybinds to the one element of thewithlist directly in front of it, which may be an&group, never to the whole list. A mixed line therefore says exactly what happens to each trait:Showabove is derived the usual way,AddandCompareare each delegated tovalue.byneeds a type with exactly one field, and the name afterbynames that field. A type with more than one field has no single answer for what a delegatedAddwould even do.Does not compiletype Money with Add by amount { amount: Int currency: String } // error: `by` needs a type with a single field: `Money` has `amount` and `currency`Where a forwarded signature mentions
Self, arguments are unwrapped and results wrapped again.add(other: Self): SelfonIntbecomesadd(other: Seconds): SecondsonSeconds, so the caller never sees the underlyingInt.A trait without
Selfin its signature can be delegated by a type of any field count, because there is nothing to unwrap or rewrap.Iterate<User>delegated by a multi-fieldTeamis this case.byforwards the required members; the default members still come from the trait. A default written in terms of the required ones -Compare.maxin terms ofcompare- stays correct without being forwarded itself, and returns the wrapper type, not the field's type.A trait that is neither derived, delegated nor written by hand does not exist on the type.
Seconds * Secondsdoes not compile unlessMultiplyis added to thewithlist, which is deliberate: multiplying twoSecondswould be square seconds, a type this design does not want to produce silently.A single-field type has the representation of its field: no allocation, no indirection. This follows from there being nothing else to store; it is not something a program can observe.
What this is not#
A distinct type is not the same feature as a private field with a parse factory, even though the two are often
combined. by forwards existing traits from the field; a private field with its own parse is how a type
enforces an invariant (a well-formed email address) rather than delegating arithmetic.
use Show from "std/core"
type ParseError with Show {
reason: String
fn show(): String {
reason
}
}
type Email with Show by value, TryFrom<String, ParseError> {
private value: String
static fn tryFrom(text: String): Result<Email, ParseError> {
if text.contains("@") {
Ok Email(text)
} else {
Fail ParseError("missing @")
}
}
}
match Email.tryFrom("ada@example.test") {
Ok(email) => print email
Fail(problem) => print problem
}A type with cases cannot use by at all, having no field of its own to name - by needs exactly one field, and
a case-bearing type has none outside its cases.
Related#
- Type aliases - the interchangeable form
bydeliberately is not. - Traits - what
withdeclares in general. - Declaring a type - fields, visibility, and what every
typegenerates.