Steve's Roc Apps roc-apps on GitHub

Safari Ride Chapters

Chapter 12

Where clauses: asking a kind

One function, Pipeline.rendering_of, renders every kind of thing beside the road. Here it renders five of them:

A tree, a cow, a radio tower, the cat and the bird, each rendered by Pipeline.rendering_of, and each at its own distance so that it shows clearly.

One function can render every kind because of this chapter's new Roc idea, the where clause, which constrains a type variable to types that have certain methods. First, here's one kind from the inside.

A kind from the inside §

A tree needs a height and a color. The types around it hold where it stands. So the Conifer type says which tree it is, and its methods give the height and the sprite:

Conifer.roc, the whole file
## Conifer is a kind of tree. It sets the tree's height, from the ground to its
## tip, and its color, and TreeShapes draws it.
import Distance
import Sprite
import TreeShapes
import lib.RgbColor

Conifer := [Green, SmallGreen, Red, Gold].{

	## A red tree is twice a small one's height and a gold one three times,
	## sizes chosen by eye.
	scenery_height : Conifer -> Distance
	scenery_height = |conifer|
		match conifer {
			Green => green_height
			SmallGreen => small_height
			Red => small_height.times(2.0)
			Gold => small_height.times(3.0)
		}

	## How a conifer looks.
	scenery_sprite : Conifer, Sprite.Setting -> Sprite
	scenery_sprite = |conifer, setting|
		TreeShapes.tree_sprite(
			setting,
			{ height: conifer.scenery_height(), foliage_color: foliage_color(conifer) },
		)

	is_eq : _
}

## A small green tree's height, chosen by eye.
small_height : Distance
small_height = Distance.metres(4.896)

## A green tree's height, 1.3 times a small one's, a size chosen by eye.
green_height : Distance
green_height = small_height.times(1.3)

green_color : RgbColor
green_color = RgbColor.of(0x1C5A22)

gold_color : RgbColor
gold_color = RgbColor.of(0xCF9A18)

red_color : RgbColor
red_color = RgbColor.of(0xB23A2A)

## A conifer's foliage color.
foliage_color : Conifer -> RgbColor
foliage_color = |conifer|
	match conifer {
		Green | SmallGreen => green_color
		Gold => gold_color
		Red => red_color
	}

Conifer := makes a nominal type, as BendAnimal is in the “Tag unions: billboard critters” chapter: four tags, and a name that keeps them apart from any other union of the same tags. The functions inside the type's braces, scenery_height and scenery_sprite, are its methods, and a value calls one with a dot: conifer.scenery_height(). Everything outside the braces is private to the module, such as the heights, the colors and foliage_color, and a call names it plainly: foliage_color(conifer). In the match, Green | SmallGreen => gives both greens one color.

scenery_sprite hands the height and the color to TreeShapes.tree_sprite, the function that builds the far tree's sprite. Why the two methods sit inside the braces is the rest of this chapter.

One function for every kind §

rendering_of takes a sighting, with the focal length and the darkness that the whole frame shares, and returns a Rendered, the thing's sprite and its depth. The sprite comes from the kind, so rendering_of calls the kind's own scenery_sprite method.

The Rendered coming out is the same type for every kind. The sighting going in carries its kind: it's a Pipeline.Sighted(kind), and kind is a type variable, so it stands for any type at all. Only some types have a scenery_sprite. So rendering_of needs a constraint on kind: any type, as long as that type has a scenery_sprite.

Roc writes that constraint as a where clause, on the line after the annotation:

Pipeline.roc
## The rendering of `sighting` through the frame's lens, in its light.
rendering_of : Pipeline.Sighted(kind), Sprite.LensAndLight -> Rendered
	where [kind.scenery_sprite : kind, Sprite.Setting -> Sprite]
rendering_of = |sighting, { focal_length, darkness }|
	Rendered.of(
		sighting.spot.foot().forward(),
		sighting.kind.scenery_sprite({ spot: sighting.spot, focal_length, darkness }),
	)

This table explains the code's concepts:

Concept Purpose Explanation
A type variable Generality kind in Pipeline.Sighted(kind) stands for whatever type the caller's sighting holds.
A where clause Constraint where [kind.scenery_sprite : kind, Sprite.Setting -> Sprite] constrains kind to types that have a method named scenery_sprite, which takes a kind and a Sprite.Setting and returns a Sprite. The square brackets hold a list of constraints, here just one.
A method Placement A method is a function inside a type's braces, and only a method meets a where clause. So the Conifer module puts scenery_sprite inside the braces, with scenery_height, and its private helpers outside.
A method call Dispatch sighting.kind is the sighting's kind field, which holds a value of the type kind. .scenery_sprite(...) calls that type's method, with the value before the dot as its first argument.

The rest of the body builds the setting: the focal length and the darkness the frame shares, and the sighting's own spot.

The compiler checks each call §

Each call to rendering_of fills kind with one particular type. The ride renders its trees in one line of Scene.scene_in_view. There trees_seen is a List(Pipeline.Sighted(Conifer)), so the type is Conifer:

Scene.roc, in scene_in_view
# ...
trees: trees_seen.map(|tree| Pipeline.rendering_of(tree, lens_and_light)),
# ...

The compiler looks inside Conifer's braces, finds a scenery_sprite of the right type, and the call compiles. The compiler checks every other call the same way, so it picks which scenery_sprite runs for each type when the program compiles. Every call returns a Rendered, whatever the kind, so a frame's renderings of trees, cows and towers all go in one list.

Every type that fills kind has to pass the compiler's check. Here a program hands rendering_of a Rock, a kind that is just one tag. The program is a small app of its own, which uses Safari Ride's modules as its game package. Only Rock and rock_rendering matter here. A name that ends in !, such as main!, marks a function with effects, which the “Pure functions and effects” chapter explains, and the _ names an argument that main! leaves unused:

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

import game.Pipeline
import game.Rendered
import game.Sprite

## A kind of thing beside the road, with one tag and no methods.
Rock := [Boulder]

rock_rendering : Pipeline.Sighted(Rock), Sprite.LensAndLight -> Rendered
rock_rendering = |sighting, lens_and_light| Pipeline.rendering_of(sighting, lens_and_light)

main! = |_| Ok({})
roc check on kind_without_sprite.roc
── ✗ missing method ────────────────────────────── kind_without_sprite.roc:11:45

A where clause requires the scenery_sprite method here, but the type being used
doesn't have that method.

rock_rendering = |sighting, lens_and_light| Pipeline.rendering_of(sighting, lens_and_light)
                                            ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

The value's type, which does not have a method named scenery_sprite, is:

    Rock

Hint: For this to work, the type would need to have a method named
scenery_sprite associated with it in the type's declaration.

── 1 error and 0 warnings ────────────────────────────── kind_without_sprite.roc

The message names the method the clause requires, and the type the compiler checked. The fix is a .{ } after Rock's tags with a scenery_sprite inside, as Conifer has.

Asking for a height §

Sighting a thing needs its height. A far thing shows only when its top clears the horizon, and only when it stands tall enough on the screen to be worth showing, two tests the next chapter shows. So Pipeline.scenery_sighting_of, which sights the trees, the billboard critters, the cat and the bird, constrains its kind with a where clause of its own, to types that have a scenery_height:

Pipeline.roc, in scenery_sighting_of
# ...
where [kind.scenery_height : kind -> Distance]
# ...

Names that say what they're for §

Roc matches a where clause by the method's name and type alone. Suppose the clause named a method called just height. The bird could well have one that says how high it flies, and the sighting would test the bird against the wrong number, yet the program would compile. So every method the pipeline's clauses name starts with "scenery", which makes an accidental match unlikely.

So a kind meets the pipeline with two methods inside its braces, scenery_height and scenery_sprite. The tree, the cow, the radio tower, the cat and the bird each have both, and the compiler checks both methods of every kind when the program compiles.

The next chapter takes the sightings that are refused.