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.
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 throughRules#
A
privatefield without a default makes the type a capsule. The constructor takes every field, aprivatefield cannot be passed from outside and a field without a default cannot be left out, so outside code has no call it could write. Aprivatefield 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 farprivatereaches (Fields, rule 5) -Pathofstd/pathis the capsule the standard library writes:Does not compileuse Path from "std/path" const wrong = Path(None, []) // error: `Path` cannot be constructed here: `rootValue` is private and has no defaulttype 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)A capsule is four things written together:
privatefields with no default, astatic fnfactory 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 isDecode, and rule 6 is the message it gets.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, plusValue, orValuesfor a plural:private componentValuesnext tofn components(),private rootValuenext tofn root()- so that a reader sees at a glance which of the two is the storage.Show,Equals,Hashandcomparestill 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 ownShow.EncodeandDecodecome from the capsule's one conversion pair. The pair is the one typeSourcefor which both directions exist:SelfhasTryFrom<Source, Failure>orFrom<Source>, andSourcehasFrom<Self>. A value is then written as itsSourceand read by decoding aSourceand handing it to the way in, so a decoded value went through the same check as every other one. ATryFromthat refuses becomes aDecodeErrorthat carries itsshow(). Where there are several such types, the pair withStringis the one: a capsule that writes as text is what every format wants, and adding a second pair later does not takeDecodeaway.With no pair, or with several and none of them
String, there is noDecode. The reflexiveFrom<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.Encodeis not refused with it. It is the way out, it cannot break an invariant, and it is whatrendered(value)and a failingassertshow - the same job the generatedShowdoes, which prints private fields too - so without a pair it stays the field-wise one.copyand patterns are closed with the constructor.value.copy(...)andSlug(text)as a pattern both name fields, and aprivatefield 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.
type Probe {
count: Int = Int.tryFrom("1")?
}
// error: `?` cannot stand in a field default: building a value never failstype 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:
use Path from "std/path"
const wrong = Path(None, [])
// error: `Path` cannot be constructed here: `rootValue` is private and has no defaultRelated#
- Construction - the generated constructor, and when it is usable from outside.
- Fields -
privateandprotected var, the modifiers that decide this. - Conversions -
FromandTryFrom, which the conversion pair is made of. - Copy and equality - what stays generated for a capsule, and what does not.