Steve's Roc Apps roc-apps on GitHub

Safari Ride Chapters

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:

clock_plus_seven.roc (doesn't compile)
app [main!] { game: "../../game.roc", lib: "../../../lib/main.roc" }

import game.Clock

main! = |_args| {
	later = Clock.start + 7
	echo!(later.to_str())
	Ok({})
}
roc check on clock_plus_seven.roc
── ✗ 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:

Clock.roc, the whole file
## 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_since 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.