Declaring a type
One keyword declares every data type. Fields are const unless marked var, members are public unless marked private, and Equals, Hash, Show and copy are generated.
Diese Seite gibt es noch nicht auf Deutsch, deshalb steht sie hier auf Englisch. Zur englischen Seite
type declares a struct, a class, an enum and an algebraic data type - there is only one keyword, because there is only
one kind of data. A type is a value: the binding that holds it decides whether it can be changed.
Example#
type Point {
var x: Int
var y: Int
fn area(): Int {
x * y
}
var fn translate(deltaX: Int = 0, deltaY: Int = 0) {
x = x + deltaX
y = y + deltaY
}
fn translated(deltaX: Int = 0, deltaY: Int = 0): Point {
copy(x: x + deltaX, y: y + deltaY)
}
static fn square(size: Int): Self {
Self size, size
}
static origin = Point 0, 0
}
var point = Point x: 10, y: 20
point.translate deltaX: 5
print "{point} {point.area()} {Point.origin}"Syntax#
[public] [shared] type <Name>[<parameters>] [with <traits>] [where <bounds>] {
<field>
<constant>
<function>
<case>
}<field> ::= [private] [var] <name>: <Type> [= <default>]
| protected var <name>: <Type> [= <default>]
<constant> ::= [private] const <name> [: <Type>] = <expression>
<function> ::= [private] fn <name>[<parameters>](<self or nothing>, <parameters>) [: <Type>] { ... }Rules#
A field is
constunless it is markedvar. A field withoutvarnever changes after construction, not even through avarbinding. That is how a value says which part of it is fixed.A member is public unless it is marked
private. Fields, methods and constants alike. Reading a field cannot break an invariant when values are never aliased andconstis deep, so what needs protection is writing.Field Read from outside Write from outside x: Valueyes no, it is constvar x: Valueyes yes protected var x: Valueyes no private x: Valueno no protected varreads as "write-protected": everybody reads the field, and only the file of its type writes it. It hands an outsider aconstpath, andconstis deep, soconfig.routes.append(...)from outside is an error whileconfig.routescan be read and iterated.privatereaches as far as the type does. A private member is visible in the body of its type and in everyextendof that type in the same package, and nowhere else. The package is the unit of coherence, so it is the unit of privacy.Every type has exactly one constructor, generated from its fields in declaration order. It cannot be written by hand and it never contains logic. Arguments may be positional or labelled, and a field with a default may be left out.
type Account { owner: String var nickname: String = "" } const first = Account "Ada" const second = Account owner: "Alan", nickname: "al" print "{first} {second}"A field default is a constant, read in a scope without
selfand without the other fields. A literal, a collection literal, a constructor or a case of constants, a named constant, or an operator on those - no call, so the constructor runs no code. The order of the fields is not observable, and a default that is computed, or that depends on another field, is what a factory is for (Construction, rule 4).The constructor is usable from outside if and only if every
privatefield has a default. Everything else is a static factory function: a member declaredstatic.type Email with TryFrom<String, String> { private value: String static fn tryFrom(text: String): Result<Email, String> { if !text.contains("@") { return Fail "'{text}' is not an email address" } Ok Self(text) } } const email = Email.tryFrom "info@example.test" print emailEquals,Hash,Showandcopyare generated, each of them only if every field supports it. A type with a function in a field has no generatedEquals. There is no identity, so==is always about content.copyhas the shape of the constructor with every field optional.point.copy(y: 30)answers a new value;privatefields are not passable from outside.A method has a receiver, a
static fnhas none.value.member(args)isType.member(value, args)when the member reads its receiver.var fnmarks a method that changes its receiver in place.A verb changes in place and its participle returns a changed copy.
translateis avar fnand answers nothing;translatedreads its receiver and answers aPoint. Pick verbs whose participle is a different word.Kompiliert nichttype Point { var x: Int var y: Int var fn translate(deltaX: Int) { x = x + deltaX } } const fixed = Point 1, 2 fixed.translate 5 // error: `translate` needs a `var`A type has one namespace of members. A field and a method cannot share a name, because a method is a constant of the type that holds a closure. That is also why calling a function held in a field always needs parentheses:
onClick()calls it,onClick { ... }assigns it.Selfis the type itself, usable in every type position inside the declaration and as the constructor (Self(size, size)). Inside the type all fields can be passed,privateones included.The generated
Showhas a fixed format, because two implementations of the language are compared through it: a type shows asType(field: value, ...)with all fields in declaration order, aListas[a, b], aMapas["k": v]([:]when empty), aSetas{a, b}, anOptionasSome(x)orNone. Inside such a value aStringis quoted and aCharis in single quotes.A
shared typehas an identity instead of a value. Assigning it does not copy,Equals,Hash,copyandEncodeare not generated, andisSame(a, b)compares identity. Handles to the outside world are shared types; almost nothing else should be.
What this is not#
There are no getters, setters or properties. A field is storage and a method computes, and the () tells a reader
which one it is. There is no get prefix; a predicate is isEmpty() or hasErrors(), a mutator is a verb written
var fn.
type Account {
protected var balance: Int = 0
var fn deposit(amount: Int) {
balance = balance + amount
}
}
var account = Account()
account.deposit 100
print account.balanceFrom another file the field is readable and nothing else - private reaches as far as the file that declares it:
use Workspace from "std/project"
var workspace = Workspace()
workspace.memberPatterns = ["packages/*"]
print workspace.memberPatterns
// error: `memberPatterns` is write-protected: only the file that declares `Workspace` writes itA constructor is not a place for logic. Validation, parsing and conversion are static factory functions, because a
constructor cannot fail in a language without exceptions. Email.tryFrom answers a Result; Email(...) cannot exist from
outside, because value is private and has no default.
A field is not a candidate for becoming a method later. A field is a promise: it is a parameter of the generated
constructor, a position in patterns, a parameter of copy, part of the generated Encode and a step of var paths.
What might one day be computed, cached or validated is a method from the start.
private is not per file. It is per type, and it extends to every extend of that type in the same package. A
top-level declaration's visibility is a different question, spelled public.
Related#
- Bindings - what
constandvardecide about a value. - Cases and match - a type with cases, and how to take one apart.
- Traits - how a type comes
witha capability. - Types and methods - the same material as a learning step.
- Why there are no properties - the argument behind rule 11.