Steve's Roc Apps roc-apps on GitHub

Safari Ride Chapters

Chapter 13

Errors as tag unions

Every frame, the pipeline tries to sight each thing on the road ahead, and its sighting functions refuse some of them. For a thing behind the camera, a thing the horizon hides or a thing too small to see, the sighting returns an Err. This chapter follows an answer that can fail, up through the functions that ask for it.

Here are some things from step 1,529 and what their sightings return. right and forward place each thing's foot from the camera, in metres, and a forward below zero is behind it. A thing less than 0.4 metres in front of the camera, short of what we call its near plane, counts as behind it too:

What it is right forward Its sighting
The cat, frozen in the road 0.38 23.63 kept
The nearest tree behind the camera -8.09 -8.36 out of sight: behind the camera
The nearest tree the horizon hides, a small one -1,433.43 446.00 out of sight: below the horizon
A big tree just past it -1,446.07 473.21 kept
The next radio tower 20.40 355.61 kept

A small tree far off sinks below the horizon, while a taller one just past it still shows its top. The ride's ground curves on a radius of 100 km, so its horizon is near. Of the 214 trees on the segments in view, 136 are kept, 30 are behind the camera, and 48 are below the horizon. The program returns one answer for every refusal, OutOfSight.

Each test lives in its own function, and each function asks the one below it first. A radio tower takes two of the tests, the near plane and the horizon, so here's its stack:

Pipeline.sighting_above_horizon_of tests the horizon Pipeline.sighting_of  passes up  an Err(OutOfSight) RiderView.rider_point_ahead tests the near plane  passes up an Err(BehindCamera),  renamed OutOfSight Pipeline.sighting_above_horizon_of tests the horizon Pipeline.sighting_of  passes up  an Err(OutOfSight) RiderView.rider_point_ahead tests the near plane  passes up an Err(BehindCamera),  renamed OutOfSight
How a refusal travels

An answer that can fail §

A sighting function's answer is a Try, Roc's type for an answer that can fail. A Try is one of two tags: Ok, holding the answer, or Err, holding a tag that says what went wrong. Here's the first sighting function, whole:

Pipeline.roc
## `placed` as a camera at `vantage` sees it, on the segment that lies at
## `from_rider`, if it is at or past the camera's near plane.
sighting_of : Geom.Placed(kind),
{
	road : RoadAhead.RoadView,
	vantage : Geom.Vantage,
	from_rider : SegmentFromRider,
} -> Try(Pipeline.Sighted(kind), [OutOfSight])
sighting_of = |placed, { road, vantage, from_rider }| {
	road_point = placed.road_point
	rider_point = RoadAhead.rider_point_at(road, { vantage, from_rider, road_point })
	foot = RiderView.rider_point_ahead(rider_point) ? |BehindCamera| OutOfSight
	road_heading = RoadAhead.road_heading_at(road, { vantage, from_rider, road_point })
	spot = Spot.at({ lift: placed.lift, foot, road_heading })
	Ok({ spot, kind: placed.kind })
}

Its type ends in Try(Pipeline.Sighted(kind), [OutOfSight]): a sighting, or OutOfSight. The last line wraps the spot and the kind in an Ok.

RiderView.rider_point_ahead gives back a Try of its own, and the ? after it handles both answers. When the answer is an Ok, the ? unwraps it, and foot is the point ahead. When it's an Err, the ? ends sighting_of and gives back an Err. So every line after the ? runs only for a thing in front of the camera.

rider_point_ahead takes a point seen from the rider, and its refusal says what's wrong with that point:

RiderView.roc
rider_point_ahead : RiderView.RiderPoint -> Try(RiderView.RiderPointAhead, [BehindCamera])

A sighting's callers treat every refusal alike, as out of sight. So the ? has a function after it, |BehindCamera| OutOfSight, and the Err's tag goes through that function on its way up. The function's argument is written as the tag itself, a pattern like the ones a match lists.

A test on top §

The next function asks sighting_of first, then makes a test of its own:

Pipeline.roc
## The sighting of `placed` on the segment at `from_rider`, if some of it shows
## over the horizon: its top, its kind's height above its lift and lowered
## by the ground's drop, is not hidden behind the curve of the ground. Every
## kind with a height takes this test here.
sighting_above_horizon_of : Geom.Placed(kind),
{
	road : RoadAhead.RoadView,
	vantage : Geom.Vantage,
	from_rider : SegmentFromRider,
} -> Try(Pipeline.Sighted(kind), [OutOfSight])
	where [kind.scenery_height : kind -> Distance]
sighting_above_horizon_of = |placed, { road, vantage, from_rider }| {
	sighting = sighting_of(placed, { road, vantage, from_rider })?
	spot = sighting.spot
	top = spot.lift().plus(placed.kind.scenery_height()).minus(spot.drop())
	if EarthCurve.is_below_horizon({ point: spot.foot().to_rider(), top }) {
		Err(OutOfSight)
	} else {
		Ok(sighting)
	}
}

The first line asks sighting_of, and its plain ? passes an Err(OutOfSight) straight up. Then the function tests the horizon, with the kind's height from its where line, as in the “Where clauses: asking a kind” chapter. drop() is how far the curving ground has dropped below level at the thing's foot. The function gives back the same tag for its own refusal, so both functions have the same type after the arrow.

Pipeline.scenery_sighting_of stacks one more test on top in the same way: a thing that would stand under 2 pixels tall on the screen is out of sight too. The trees, the billboards, the cat and the bird take all three tests. A radio tower is kept however small it looks, so it's sighted by sighting_above_horizon_of.

Where the refusals stop §

A segment holds many trees, so its sighting module asks for each tree's sighting and keeps the ones that come back:

TreeSighting.roc
## The trees in view on a segment in view.
segment_trees : RoadAhead.RoadView,
{
	camera : Pipeline.SightingCamera,
	in_view : RoadAhead.SegmentInView,
} -> List(Pipeline.Sighted(Conifer))
segment_trees = |road, { camera, in_view }|
	in_view.segment.trees.keep_oks(
		|tree| Pipeline.scenery_sighting_of(tree, { road, camera, from_rider: in_view.from_rider }),
	)

keep_oks calls the function it's given on each item of a list, keeps the Ok values, and drops the Errs. So segment_trees gives back a plain list of sightings, and the refusals stop there.

There's one truck, so Scene matches on its answer instead:

Scene.roc
## The truck's renderings: one when its sighting kept it, and an empty list
## when it is out of sight.
truck_renderings : TruckSighting.TruckFate, Sprite.LensAndLight -> List(Rendered)
truck_renderings = |fate, lens_and_light|
	match fate {
		Ok(truck) => [Pipeline.rendering_of(truck, lens_and_light)]
		Err(OutOfSight) => []
	}

A TruckFate is the truck's sighting, a Try, and the match covers both its tags. For an Err, the truck's renderings are an empty list.

Try or crash §

A Try is for a value that can rightly be missing, such as a tree behind the camera. Other facts hold whenever the program is right, such as a share lying between 0 and 1. A fact like that is an invariant. A value that breaks one means a bug, and a made-up answer would hide the bug until it surfaced somewhere far from its cause.

So the program stops at once instead. Fraction.of makes a share from a ratio:

lib/Fraction.roc
## A ratio that can only be in range, as a fraction.
of : F64 -> Fraction
of = |ratio|
	match from_ratio(ratio) {
		Ok(fraction) => fraction
		Err(OutOfRange) => crash "Fraction.of: this ratio is outside 0 to 1, so the sum behind it is wrong"
	}

from_ratio returns a Try, and the match covers both tags. For an Err, crash stops the program with a message that says which invariant broke. Use crash to enforce invariants.

So a refusal is an ordinary value: a ? passes it up, a function after the ? can rename it, and a keep_oks or a match is where it stops. A value missing only through a bug gets a crash instead. The next chapter starts on the world the things stand in, with its sky and its grass.