Data or capsule

A type is data, whose constructor is the way in, or a capsule, whose constructor a private field without a default closes - and then a factory, accessors and one conversion pair take its place.

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

Every type is one of two things, and one private decides which. Data has a constructor anybody can call, so a value of it is exactly its fields. A capsule has at least one private field without a default, so nothing outside can call the constructor at all, and a static fn factory is the one way to a value - which is where an invariant is checked, a text is parsed, or a value is normalized.

A capsule is a standard and not a construct of the language. There is no keyword for it: it is four ordinary things written together.

Example#

type Title {
  text: String
}

type Slug with From<Title> {
  private value: String

  static fn from(value: Title): Slug {
    Self value.text.toLowerCase().replace(" ", "-")
  }

  fn text(): String {
    value
  }
}

extend Title with From<Slug> {
  static fn from(value: Slug): Title {
    Title value.text()
  }
}

const slug = Slug.from Title("Data Or Capsule")
print slug.text()

Syntax#

type <Name> {                            // Data: the constructor is the way in
  <field>: <Type>
}

type <Name> {                            // A capsule: a private field without a default closes the constructor
  private value: <Type>                  // the one stored field is named `value`

  static fn from(<source>): Self         // ...so a factory is the way in
  fn <name>(): <Type>                    // ...and an accessor is the way out
}

extend <Source> with From<<Name>>        // ...and the pair is what `Encode` and `Decode` are derived through

Rules#

  1. A private field without a default makes the type a capsule. The constructor takes every field, a private field cannot be passed from outside and a field without a default cannot be left out, so outside code has no call it could write. A private field with a default leaves the constructor open: it is left out, and the type is data with a hidden part. "Outside" is another file, because that is how far private reaches (Fields, rule 5) - Path of std/path is the capsule the standard library writes:

    Kompiliert nicht
    use Path from "std/path"
    
    const wrong = Path(None, [])
    // error: `Path` cannot be constructed here: `rootValue` is private and has no default
    type Percent {
      private value: Int
    
      static fn tryFrom(value: Int): Result<Percent, String> {
        if value < 0 || value > 100 {
          return Fail "{value} is not between 0 and 100"
        }
        Self value
      }
    }
    
    print Percent.tryFrom(120)
  2. A capsule is four things written together: private fields with no default, a static fn factory that is the only way in, accessors that read what it holds, and one conversion pair that is the way in and the way back out. Nothing is generated by a keyword, and a type that has only the first three is a capsule too - what it lacks is Decode, and rule 6 is the message it gets.

  3. A field and a method never share a name, so the field of a capsule is never named for what the accessor answers. One stored field is named value, its unit or meaning said in the field's doc comment. Several stored fields are named for the method each answers, plus Value, or Values for a plural: private componentValues next to fn components(), private rootValue next to fn root() - so that a reader sees at a glance which of the two is the storage.

  4. Show, Equals, Hash and compare still read the fields, private ones included. They are the debug form and the value form of the type, neither of them can build a value, and a capsule with a secret writes its own Show.

  5. Encode and Decode come from the capsule's one conversion pair. The pair is the one type Source for which both directions exist: Self has TryFrom<Source, Failure> or From<Source>, and Source has From<Self>. A value is then written as its Source and read by decoding a Source and handing it to the way in, so a decoded value went through the same check as every other one. A TryFrom that refuses becomes a DecodeError that carries its show(). Where there are several such types, the pair with String is the one: a capsule that writes as text is what every format wants, and adding a second pair later does not take Decode away.

  6. With no pair, or with several and none of them String, there is no Decode. The reflexive From<Self> that every type has never counts. The message names the rule and what is missing:

    error: `Email` does not implement `Decode`
      = `Email` is a capsule: `value` is private and has no default, so nothing outside can call the constructor and
        `Decode` comes from `Email`'s one conversion pair instead of from its fields
      = `Email` has no pair. Write `From<Source>` or `TryFrom<Source, Failure>` for `Email`, and `From<Email>` for that
        `Source`

    With more than one candidate the second note lists them: `Slug` converts both ways with `Title` and `Label`. Exactly one type may.

    Encode is not refused with it. It is the way out, it cannot break an invariant, and it is what rendered(value) and a failing assert show - the same job the generated Show does, which prints private fields too - so without a pair it stays the field-wise one.

  7. copy and patterns are closed with the constructor. value.copy(...) and Slug(text) as a pattern both name fields, and a private field cannot be named from outside, so both are refused there. Inside the type they work as they always do.

    error: `value` is private to `Slug`, so this pattern cannot read it
      = A pattern reads the fields of a value: a type with a private field is taken apart by its own members

What this is not#

A capsule is not a constructor with logic in it. A constructor is generated in every type there is: it has no body, and it never transforms or rejects what the caller passes. A field default is a constant written at the field that stands in for an argument the caller left out (Construction, rule 4): it runs no code, and it cannot fail either, so a ? in one is refused. What a capsule adds is a factory next to the constructor that can compute, refuse or normalize, plus a private that makes the factory the only door. So the failure is in the type of the factory and not hidden in a construction.

Kompiliert nicht
type Probe {
  count: Int = Int.tryFrom("1")?
}
// error: `?` cannot stand in a field default: building a value never fails
type Celsius with TryFrom<Int, String> {
  private value: Int

  static fn tryFrom(value: Int): Result<Celsius, String> {
    if value < -273 {
      return Fail "{value} is below absolute zero"
    }
    Ok Self(value)
  }

  fn degrees(): Int {
    value
  }
}

print Celsius.tryFrom(-300)

From another file the constructor is not there at all, which is the whole point of the factory:

Kompiliert nicht
use Path from "std/path"

const wrong = Path(None, [])
// error: `Path` cannot be constructed here: `rootValue` is private and has no default
  • Construction - the generated constructor, and when it is usable from outside.
  • Fields - private and protected var, the modifiers that decide this.
  • Conversions - From and TryFrom, which the conversion pair is made of.
  • Copy and equality - what stays generated for a capsule, and what does not.
Esc