16. Capstone: a typed pipeline
This last lesson puts the course together in one small program. It takes a JSON string of race results, decodes each into a record you define (lesson 12), handles the failure branch as a value (lessons 4 and 5), folds over the list to combine them (lesson 7), and computes an average speed that the compiler checks dimensionally (lesson 14). The whole thing prints a one-line report with an f-string.
The one join worth watching is where JSON meets units. A decoded number is a plain float with no
unit attached, because JSON has no notion of metres or seconds. You attach the unit yourself by
multiplying by a unit constant like 1.0<m>, which turns a float into a float<m>. From there the
units carry through the arithmetic on their own, so dividing a folded float<m> by a folded
float<s> gives the average speed in float<m/s> without any annotation.
The exercise is the program itself. Three holes sit at the interesting joints: the decoder for the
distance field, the unit constant that lifts a decoded distance into metres, and the divisor that
turns total distance and total time into a speed. Run pyfun check and the compiler tells you the
type each hole expects, along with names in scope that fit. Fill all three and the report prints.
measure m
measure s
type Run = { name: string, distance: float, time: float }
let runDecoder =
Decode.map3 (fun n d t -> Run { name = n, distance = d, time = t })
(Decode.field "name" Decode.string)
(Decode.field "distance" ?)
(Decode.field "time" Decode.float)
let input = """[
{"name": "ada", "distance": 100.0, "time": 20.0},
{"name": "bo", "distance": 140.0, "time": 30.0}
]"""
let report =
match Decode.decodeString (Decode.list runDecoder) input:
case Ok runs:
let names = runs |> List.map (fun r -> r.name) |> String.join ", "
let dist = runs |> List.fold (fun a r -> a + r.distance * ?) 0.0<m>
let time = runs |> List.fold (fun a r -> a + r.time * 1.0<s>) 0.0<s>
let avg = dist / ?
f"{names}: average speed {avg} m/s over {List.len runs} runs"
case Error e: f"could not read input ({e.errorKind})"
print report
The checker names each expected type:
note: hole `?` has type `Decoder float` — try: Decode.float
note: hole `?` has type `float<m>` — try: a
note: hole `?` has type `float<'a>` — try: dist, time
Expected output:
ada, bo: average speed 4.8 m/s over 2 runs
Show solution
measure m
measure s
type Run = { name: string, distance: float, time: float }
let runDecoder =
Decode.map3 (fun n d t -> Run { name = n, distance = d, time = t })
(Decode.field "name" Decode.string)
(Decode.field "distance" Decode.float)
(Decode.field "time" Decode.float)
let input = """[
{"name": "ada", "distance": 100.0, "time": 20.0},
{"name": "bo", "distance": 140.0, "time": 30.0}
]"""
let report =
match Decode.decodeString (Decode.list runDecoder) input:
case Ok runs:
let names = runs |> List.map (fun r -> r.name) |> String.join ", "
let dist = runs |> List.fold (fun a r -> a + r.distance * 1.0<m>) 0.0<m>
let time = runs |> List.fold (fun a r -> a + r.time * 1.0<s>) 0.0<s>
let avg = dist / time
f"{names}: average speed {avg} m/s over {List.len runs} runs"
case Error e: f"could not read input ({e.errorKind})"
print report
Decode.float decodes the distance field, 1.0<m> lifts each decoded distance into metres so the
fold accumulates a float<m>, and dividing that by the folded float<s> gives float<m/s>. A
malformed input would take the Error branch instead of crashing, which is the whole point of
decoding at the edge. From here you have every piece the language showcases: types, matching,
records, collections, effects, decoding, computation expressions, units, and modules.