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.

Diese Seite gibt es noch nicht auf Deutsch, deshalb steht sie hier auf Englisch. Zur englischen Seite

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 raw

Syntax#

type Name with Trait, (Trait, Trait) by field, Trait by field { ... }

Rules#

  1. A distinct type is a type with a single field, unlike a type alias, which is interchangeable with its target. Seconds and Int above are never interchangeable: Seconds(5) + 2 does not type check.

    Kompiliert nicht
    type Seconds with Add by value {
      value: Int
    }
    
    const total = Seconds(5) + 2
    // error: Expected `Seconds`, found `Int64`
  2. by binds to the one element of the with list 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: Show above is derived the usual way, Add and Compare are each delegated to value.

  3. by needs a type with exactly one field, and the name after by names that field. A type with more than one field has no single answer for what a delegated Add would even do.

    Kompiliert nicht
    type Money with Add by amount {
      amount: Int
      currency: String
    }
    // error: `by` needs a type with a single field: `Money` has `amount` and `currency`
  4. Where a forwarded signature mentions Self, arguments are unwrapped and results wrapped again. add(other: Self): Self on Int becomes add(other: Seconds): Seconds on Seconds, so the caller never sees the underlying Int.

  5. A trait without Self in 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-field Team is this case.

  6. by forwards the required members; the default members still come from the trait. A default written in terms of the required ones - Compare.max in terms of compare - stays correct without being forwarded itself, and returns the wrapper type, not the field's type.

  7. A trait that is neither derived, delegated nor written by hand does not exist on the type. Seconds * Seconds does not compile unless Multiply is added to the with list, which is deliberate: multiplying two Seconds would be square seconds, a type this design does not want to produce silently.

  8. 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.

  • Type aliases - the interchangeable form by deliberately is not.
  • Traits - what with declares in general.
  • Declaring a type - fields, visibility, and what every type generates.
Esc