Chapter 23
Opaque types: the clock
Every moment of the ride carries a clock, which counts the steps since the ride started. Step 1,529, the page the first half held still, is a clock. The sun and the sky follow the clock alone, and each beacon follows the clock and its tower.
A moment and a length §
The ride uses time in two ways. A moment is a point in the ride, such as step 1,529. A length is a stretch of the ride, such as three steps. A moment plus a length is a later moment. So Safari Ride gives each its own type: a Clock is a moment, and a Steps is a length.
Both hold a whole number, so a bare number could pass for either. Here's a line that tries to add 7 to the first clock:
app [main!] { game: "../../game.roc", lib: "../../../lib/main.roc" }
import game.Clock
main! = |_args| {
later = Clock.start + 7
echo!(later.to_str())
Ok({})
}
── ✗ type mismatch ─────────────────────────────────── clock_plus_seven.roc:6:24
This number is being used where a non-number type is needed.
later = Clock.start + 7
^
Other code expects this to have the type:
Steps
── 1 error and 0 warnings ───────────────────────────────── clock_plus_seven.roc
The compiler rejects the line. + calls the plus method, and Clock.plus takes a Steps. The line passes it a bare 7. With the types right, a mixed-up sum is a compile error, found before the ride ever runs. The “Mountains” chapter splits direction the same way: a Bearing is a direction, and a Turn is a change of direction.
The module §
Here's the whole Clock module:
## The ride's clock: which step of the ride this is. It starts at zero and
## moves on by one with every step, so it is always whole. It is one of the
## four things each step carries to the next.
import Steps
Clock :: U32.{
## The clock as a ride starts.
start : Clock
start = Clock.(0)
## The clock at step `number` of a ride.
at_step : U32 -> Clock
at_step = |number| Clock.(number)
## The clock one step on.
next : Clock -> Clock
next = |Clock.(n)| Clock.(n + 1)
## How many steps this clock is past `earlier`. It crashes when this clock
## comes before `earlier`.
steps_since : Clock, Clock -> Steps
steps_since = |Clock.(n), Clock.(earlier)|
if n < earlier {
crash "Clock.steps_since: this clock comes before the one it counts from"
} else {
Steps.of(n - earlier)
}
## The clock `length` steps on.
plus : Clock, Steps -> Clock
plus = |Clock.(n), length| Clock.(n + length.to_u32())
## The clock `length` steps back. It crashes when that would go back past
## step 0, where the ride starts.
minus : Clock, Steps -> Clock
minus = |Clock.(n), length| {
back = length.to_u32()
if back > n {
crash "Clock.minus: the ride starts at step 0, and nothing comes before it"
} else {
Clock.(n - back)
}
}
is_lt : Clock, Clock -> Bool
is_lt = |Clock.(n), Clock.(other)| n < other
to_str : Clock -> Str
to_str = |Clock.(n)| n.to_str()
is_eq : _
}
expect Clock.at_step(9).steps_since(Clock.at_step(9)) == Steps.zero
expect Clock.at_step(9).steps_since(Clock.at_step(8)) == Steps.one_step
expect Clock.at_step(9).steps_since(Clock.start) == Steps.of(9)
expect Clock.start.next().steps_since(Clock.start) == Steps.one_step
expect Clock.start.plus(Steps.zero) == Clock.start
expect Clock.at_step(9).plus(Steps.of(3)) == Clock.at_step(12)
expect Clock.at_step(9).minus(Steps.zero) == Clock.at_step(9)
expect Clock.at_step(9).minus(Steps.of(3)) == Clock.at_step(6)
expect Clock.at_step(9).minus(Steps.of(9)) == Clock.start
expect Clock.at_step(8) < Clock.at_step(9)
expect !(Clock.at_step(9) < Clock.at_step(9))
expect !(Clock.at_step(10) < Clock.at_step(9))
Clock :: U32.{ … } makes Clock an opaque type. It holds a U32, and code outside the module reaches that number through the functions between the braces. Those functions keep moments and lengths apart:
| Function | Takes | Returns |
|---|---|---|
next |
a clock | the clock one step on |
plus |
a clock and a length | a later clock |
minus |
a clock and a length | an earlier clock |
steps_ |
two clocks | the length between them |
RideInstant.next calls Clock.next once a step. plus reads the number inside a length with length.to_u32(), from the Steps module. < calls is_lt, just as + calls plus, so two clocks compare as their steps do.
Two invariants §
We enforce invariants with crash, and the clock has two. Every clock is at step 0 or later, so minus crashes when it would go back past step 0. Every length counts forward, so steps_since crashes when the clock it counts from comes later. Either crash means a bug in the caller, and its message says which invariant broke.
Tests §
Each expect below the module's braces is a test, and roc test runs them all. A clock is a plain value, so a test builds one directly. Clock.at_step(9).plus(Steps.of(3)) == Clock.at_step(12) passes when step 9 plus three steps is step 12. The last three tests try < on a clock before another, a clock against itself, and a clock after another. ! flips a Bool.
The clock is the first of the four things a moment carries. The sun and the sky are derived from it alone, at any step, and the beacons from it and their towers. The next chapter explains the step itself, which moves all four on together.