Records¶
A type with fields is a record. It declares a shape: which fields a
value has, and the type of each. A record is a type of the program that
declares it, and nothing else is a Point.
Declaring and building¶
A record is declared with its fields in braces, each a name, a colon and a type, on one line separated by commas or one per line. Its constructor is its name, taking the fields by name, and a dot reads a field:
type Point { x : Int, y : Int }
p = Point(x = 1, y = 2)
[p.x, p.y]
[1, 2]
The fields may also be given in declaration order, Point(1, 2). A value
prints as its constructor call, Point(x = 1, y = 2), so what the book
shows is what builds it.
A record value is complete. Leaving a field out is a type error naming the field, and a field at the wrong type is one naming the type:
type Point { x : Int, y : Int }
Point(x = 1)
check error[type-mismatch]: `Point` needs `y`
--> <expression>:2:1
|
2 | Point(x = 1)
| ^^^^^^^^^^^^
Reading a field the record does not declare is a name error, before the program runs. A record has no hidden fields: what it declares is what a value has, and all of it is readable.
Nested records¶
A field may be of a record type, and a path reads through it:
type Point { x : Int, y : Int }
type Rectangle { top_left : Point, bottom_right : Point }
fn area(r: Rectangle) : Int = (r.bottom_right.x - r.top_left.x) * (r.bottom_right.y - r.top_left.y)
area(Rectangle(top_left = Point(x = 0, y = 0), bottom_right = Point(x = 4, y = 3)))
12
Optional fields¶
A field written email? : String may be absent. It reads as an
Option<String>, none where nothing was given, and it may be left out at
construction:
type Contact { name : String, email? : String }
[Contact(name = "Ada"), Contact(name = "Bob", email = "bob@example.com")] | map(c => c.email | unwrap_or("-"))
["-", "bob@example.com"]
Contact(name = "Ada") prints as Contact(name = "Ada", email = none).
Absence is a value of the field, and chapter 2
has the ways out of an Option.
Equality¶
Two records of one type are equal when their fields are, field by field. Nothing is declared for it:
type Point { x : Int, y : Int }
[Point(x = 1, y = 2) == Point(x = 1, y = 2), Point(x = 1, y = 2) == Point(x = 2, y = 1)]
[true, false]
A type that wants another equality implements Eq, as
operators shows.
Generic records¶
A record may take type parameters, and takes them from the values it is
built from: Stack(items = [1, 2]) is a Stack<Int>, and
Stack(items = ["a"]) a Stack<String>. A function over it names the same
parameter:
type Stack<T> { items : List<T> }
fn push<T>(s: Stack<T>, x: T) : Stack<T> = Stack(items = concat(s.items, [x]))
fn peek<T>(s: Stack<T>) : Option<T> = s.items.last()
s = Stack(items = [1, 2]) | push(3)
peek(s)
3
Pushing "three" onto a Stack<Int> is a type error: within one use a parameter
names one type in every position it occurs. The prelude's Pair<A, B> is a
generic record, the element zip and enumerate produce, and
Pair(first = p.second, second = p.first) swaps one.
A parameter may carry a bound, written after the fields:
type Box<T> { value : T } where T : Orderable. A Box of a set is then a
type error at construction, since a set has no order. Generics
is about parameters and bounds in full.
A record under another¶
A record declared under another record, type Person : Named { born : Int },
has the fields of both. A Person is a Named, so it is taken wherever a
Named is expected, as the chapter's first program showed, and
a contract's rules are inherited the same way
(type contracts).
Constructors a program declares¶
Beside the constructor its fields give it, a type may declare constructors
of its own: a function named after the type, in the unit that declares the
type, returning it. Temp below is built from a temperature in Celsius as
well as from its field:
type Temp { kelvin : Float }
fn Temp(celsius: Float) : Temp = Temp(kelvin = celsius + 273.0f)
[Temp(1.0f).kelvin, (2.0f | Temp).kelvin, Temp(kelvin = 5.0f).kelvin]
[274.0, 275.0, 5.0]
A constructor is an operation like any other, so it has two forms: called,
Temp(1.0f), and as a pipeline step, 2.0f | Temp, where the piped value
is its first argument. It is also a function value: xs | map(Temp) builds
one per element. A type may have several constructors, told apart by the
type of their first parameter.
A function in UpperCamelCase is a constructor and nothing else. One named
after no declared type is a type error, and the message says to name it in
snake_case. The standard constructors follow the same rule: List(xs),
Set(xs) and Map(keys, values) are declared beside their types, and
subtypes and narrowing uses them as the conversion between
collections.