Structs, enums and traits
Three ways to make a type: a struct holds several values at once, an enum holds one of several cases, and a trait names what a set of types can do. With generics, they are the whole type system a program needs.
/// A struct: named fields, a value type.
struct Point derive(Eq) { x: f64, y: f64 }
impl Point {
fn origin() -> Point { return Point{ .x = 0.0, .y = 0.0 } }
fn distance(self: *Self, other: *Point) -> f64 {
let dx = self.x - other.x
let dy = self.y - other.y
return math.sqrt(dx * dx + dy * dy)
}
fn shift(self: *mut Self, dx: f64, dy: f64) {
self.x += dx
self.y += dy
}
}
/// An enum: one of several cases, some with data.
enum Shape {
Circle(f64),
Rect { w: f64, h: f64 },
Triangle(f64, f64, f64),
Empty,
}
fn area(s: *Shape) -> f64 {
match s.* {
.Circle(r) => math.PI * r * r,
.Rect(w, h) => w * h,
.Triangle(a, b, c) => {
let p = (a + b + c) / 2.0
math.sqrt(p * (p - a) * (p - b) * (p - c))
},
.Empty => 0.0,
}
}
/// A trait: a set of methods a type promises.
trait Named {
fn name(self: *Self) -> String
}
impl Named for Point {
fn name(self: *Self) -> String { return format("point ({}, {})", .{self.x, self.y}) }
}
impl Named for Shape {
fn name(self: *Self) -> String {
return match self.* {
.Circle(_) => String.from("circle"),
.Rect(_, _) => String.from("rectangle"),
.Triangle(_, _, _) => String.from("triangle"),
.Empty => String.from("nothing"),
}
}
}
/// A generic function: `T` is a type parameter, checked once per use.
fn largest(comptime T: type where T: Ord, items: []T) -> ?T {
if items.len == 0 { return null }
var best = items[0]
for x in items { if x > best { best = x } }
return best
}
/// A generic struct.
struct Pair(T) { first: T, second: T }
impl(T) Pair(T) {
fn swapped(self: *Self) -> Pair(T) { return Pair(T){ .first = self.second, .second = self.first } }
}
fn describe(thing: dyn Named) {
println("this is a {}", .{thing.name()})
}
fn main() {
var p = Point{ .x = 3.0, .y = 4.0 }
let o = Point.origin()
println("{} from the origin", .{p.distance(&o)})
p.shift(1.0, 1.0)
println("moved to ({}, {}); equal to origin: {}", .{p.x, p.y, p == o})
let shapes = [Shape.Circle(1.0), Shape.Rect{ .w = 2.0, .h = 3.0 }, Shape.Triangle(3.0, 4.0, 5.0), Shape.Empty]
for s in shapes { println("{}: {:.2}", .{s.name(), area(&s)}) }
let numbers = [3, 9, 4]
println("{}", .{largest(i32, numbers[..]) orelse 0})
let none = List(f64).new()
println("{}", .{largest(f64, none[..]) orelse -1.0})
let words = ["pear", "apple", "fig"]
println("{}", .{largest([]u8, words[..]) orelse ""})
let pair = Pair(i32){ .first = 1, .second = 2 }
let sw = pair.swapped()
println("{} {}", .{sw.first, sw.second})
// trait objects: one function, any type that implements the trait
describe(&p)
describe(&shapes[0])
}
5.0 from the origin
moved to (4.0, 5.0); equal to origin: false
circle: 3.14
rectangle: 6.00
triangle: 6.00
nothing: 0.00
9
-1.0
pear
2 1
this is a point (4.0, 5.0)
this is a circle
Structs and methods#
struct Point derive(Eq) { x: f64, y: f64 }
A struct is a value: assigning one copies its fields, and two Points are two Points. Fields are separated by commas or newlines and may have defaults (verbose: bool = false). derive(Eq) makes == work field by field; derive(Ord) adds < and friends, and derive(Hash) lets the type be a map key. derive(Clone) gives it .clone(), a deep copy field by field, once every field can clone.
Methods live in an impl block. The receiver is spelled out:
fn origin() -> Point: no receiver, an associated function, called asPoint.origin().fn distance(self: *Self, ...): a method that reads the struct;Selfis the type of theimpl.fn shift(self: *mut Self, ...): a method that changes it, which only avar(or something reached through*mut) can call.
p.distance(&o) passes a pointer to o because the parameter is *Point; p.shift(1.0, 1.0) takes &mut p for you. Field access through a pointer needs no arrow: self.x and other.x both just work.
Enums#
enum Shape {
Circle(f64),
Rect { w: f64, h: f64 },
Triangle(f64, f64, f64),
Empty,
}
An enum value is exactly one of its cases, and a case may carry data: a tuple of values (Circle(f64)), named fields (Rect { w, h }), or nothing (Empty). Construct with Shape.Circle(1.0) or Shape.Rect{ .w = 2.0, .h = 3.0 }. The only way to look inside is match:
match s.* {
.Circle(r) => math.PI * r * r,
.Rect(w, h) => w * h,
...
.Empty => 0.0,
}
.Circle(r) binds the payload. The match must cover every case; leave one out and the compiler names it. That is the point of enums over "an integer tag and some fields": adding a case later makes every match that forgot it a compile error, not a silent fall-through.
s.* reads through the pointer s: *Shape. Matching through a pointer binds the payloads by reference, which matters when they own something (a List inside a case is not copied; chapter 8).
An enum whose cases are all empty (a plain "one of these") casts to an integer with as and back with a match.
Traits and dyn#
trait Named {
fn name(self: *Self) -> String
}
impl Named for Point { ... }
impl Named for Shape { ... }
A trait is a list of method signatures. impl Trait for Type provides them, and from then on p.name() works on a Point. Traits are how generic code states what it needs (where T: Ord below), and how one function can accept different types at run time:
fn describe(thing: dyn Named) {
println("this is a {}", .{thing.name()})
}
dyn Named is a trait object: a pointer to any value whose type implements Named, plus a table of that type's methods. describe(&p) and describe(&shapes[0]) pass a Point and a Shape through the same parameter, and the call dispatches at run time. A List(dyn Named) holds a mixed collection. Trait objects carry effects too: dyn Named !allocates accepts only implementations that do not allocate (chapter 15).
Generics#
fn largest(comptime T: type where T: Ord, items: []T) -> ?T {
A generic function takes a type as a comptime parameter. where T: Ord says the function will compare values, so only ordered types are accepted; the call names the type, largest(i32, numbers[..]), and the compiler generates one version of the function per distinct type it is called with (monomorphization: no boxing, no runtime type information). Generic structs look the same: Pair(T), constructed as Pair(i32){ ... }, with methods in impl(T) Pair(T).
The standard library's std.lists is written this way (lists.map(i32, i32, xs[..], double)), which is why its functions take the element type first.
Other declarations#
Two you will meet in later chapters and in other people's code:
type Meters = distinct f64 // a new type with f64's representation and no implicit conversion
record Dose { mg: f64 where value > 0.0 } // a struct whose invariants are checked when it is built
ref class Node { value: i32, next: ?Node } // a reference-counted object (chapter 8)
Next: ownership, the chapter that makes the language what it is.
Exercises#
The compiler grades these: nx topo 7 writes them into a directory, nx topo check runs them. Or do them here: the code is editable, Run runs it with the compiler in the page (it loads on the first run and never sends your code anywhere), Check grades it as nx topo check would, and Reset puts the exercise back. Ctrl+Enter runs.
fill_cornersfill in the blank
Replace each ??? with the pattern that matches the variant, so every shape reports its corners.
enum Shape { Circle(f64), Rect { w: f64, h: f64 }, Triangle(f64, f64, f64) }
fn corners(s: *Shape) -> u32 {
return match s.* {
??? => 0,
.Rect(_, _) => 4,
??? => 3,
}
}
fn main() {
let shapes = [Shape.Circle(1.0), Shape.Rect{ .w = 2.0, .h = 3.0 }, Shape.Triangle(3.0, 4.0, 5.0)]
for s in shapes { println("{}", .{corners(&s)}) }
}
The recorded output
0
4
3
Hints
- an arm names the variant with a leading dot and
_for each payload it ignores
Solution
enum Shape { Circle(f64), Rect { w: f64, h: f64 }, Triangle(f64, f64, f64) }
fn corners(s: *Shape) -> u32 {
return match s.* {
.Circle(_) => 0,
.Rect(_, _) => 4,
.Triangle(_, _, _) => 3,
}
}
fn main() {
let shapes = [Shape.Circle(1.0), Shape.Rect{ .w = 2.0, .h = 3.0 }, Shape.Triangle(3.0, 4.0, 5.0)]
for s in shapes { println("{}", .{corners(&s)}) }
}
write_areawrite it
Give Square and Circle an Area implementation so the tests pass.
trait Area {
fn area(self: *Self) -> f64
}
struct Square { side: f64 }
struct Circle { r: f64 }
???
fn main() {
let sq = Square{ .side = 3.0 }
let c = Circle{ .r = 1.0 }
println("{:.2} {:.2}", .{sq.area(), c.area()})
}
test "square" {
let s = Square{ .side = 2.0 }
expect_eq(s.area(), 4.0)
}
test "circle" {
let c = Circle{ .r = 2.0 }
expect(c.area() > 12.56 and c.area() < 12.57)
}
The recorded output
9.00 3.14
Hints
impl Area for Square { fn area(self: *Self) -> f64 { ... } };math.PIis the constant
Solution
trait Area {
fn area(self: *Self) -> f64
}
struct Square { side: f64 }
struct Circle { r: f64 }
impl Area for Square {
fn area(self: *Self) -> f64 { return self.side * self.side }
}
impl Area for Circle {
fn area(self: *Self) -> f64 { return math.PI * self.r * self.r }
}
fn main() {
let sq = Square{ .side = 3.0 }
let c = Circle{ .r = 1.0 }
println("{:.2} {:.2}", .{sq.area(), c.area()})
}
test "square" {
let s = Square{ .side = 2.0 }
expect_eq(s.area(), 4.0)
}
test "circle" {
let c = Circle{ .r = 2.0 }
expect(c.area() > 12.56 and c.area() < 12.57)
}