Session 5: Reaching the Python ecosystem
Objectives
By the end of this session, students can:
- Import a real Python callable with
externand give it a Pyfun type, usingextern purewhere a call is genuinely side-effect free. - Decode untrusted JSON into a typed record or a structured error with the
Decodemodule. - Rewrite a staircase of
Resultmatches as a flatresult { }computation expression usinglet!andreturn. - Tag a number with a unit of measure and let the compiler check dimensions, then explain that units erase at lowering.
- Group definitions into a module and qualify calls as
Module.member. - Assemble a small end-to-end pipeline that decodes, folds, and computes a unit-checked result.
Prerequisites
Sessions 1 through 4. The capstone leans on records (Session 3), Result and match (Session 2),
and folds (Session 3), so this session is best after the whole unit.
Six-session variant
If you are running the unit as six sessions, split here. Session 5 becomes lessons 12 and 13 (extern and decoding, then computation expressions), and Session 6 becomes lessons 14 through 16 (units, modules, and the capstone), with the capstone as the finale. The demo script below is ordered so the split falls cleanly after step 4.
Demo script
The thread is the boundary versus the engine: Pyfun earns its keep where the outside world is untyped and can fail, and it stays out of the way of fast native code.
- Open the Lesson 12 exercise.
Run
checkand read the?titleDecnote: it has typeDecoder stringand suggestsDecode.string. Fill both holes (Decode.string,Decode.int) and Run. The well-formed object becomes a typedBook, and the object missingpagesbecomesfailed (KeyError): 'pages'. - Make the boundary-versus-engine point: the JSON is the untyped, failable edge, and decoding turns
it into a
Bookor a structured error before the rest of the program sees it. Contrast with wrapping something like numpy, where Pyfun adds little because the speed lives in native code. - Open the Lesson 13 exercise.
Before filling it, show the staircase form from the top of lesson 13 (two nested
Resultmatches) and point out that everyErrorbranch does the same thing. Then fill the two holes withOption.toResult "not a number" (String.toInt a)and itsbtwin and Run. - Make the point that
let!unwraps anOk,returnwraps the final value, and the short-circuit on the firstErroris automatic. Theresult { }block is the staircase written once. (If you are splitting into six sessions, this is the break point.) - Open the Lesson 14 exercise.
It adds a distance to a time. Run
checkand read the mismatch:expected float<m>, found float<s>. Change the+to/and Run to get8.0. Show the Python panel: the units are gone, the output is plaindistance / elapsed. Units are checked, then erased. - Open the Lesson 15 exercise.
Wrap the two functions in
module Temp =and qualify the calls asTemp.cToFandTemp.fToC. Run. Note that this in-file module runs in the playground, while one-module-per-file projects need the installed compiler. - Finish with the capstone. Open the Lesson 16 exercise.
Run
checkand let the three hole notes name each type:Decoder float,float<m>,float<'a>. Fill them (Decode.float,1.0<m>,time) and Run to get the one-line report. Point at the join where JSON meets units: a decoded number is a plainfloat, and multiplying by1.0<m>lifts it into metres so the fold carries afloat<m>. - Close the unit by naming what the capstone touched: types, matching, records, collections, decoding, computation expressions, units, and modules. Every piece the language showcases, in one small program that fails safely on bad input.
Assigned exercises
- In class: the Lesson 12 exercise and the Lesson 13 exercise, driven from the demo.
- Homework: the Lesson 14 exercise and the Lesson 15 exercise.
- Capstone: the Lesson 16 exercise, fill all three holes. Suitable as an
in-class finale or a take-home assessment. As a stretch, ask students to feed the decoder a
malformed JSON string and confirm the
Errorbranch reports rather than crashes.
Common misconceptions
- “
externmeans the Python call is safe now.” Correction:externgives the call a type, but the boundary is effectful by default and can still raise. Wrap a call that can fail intry, or decode the data, to turn failure into a value. - “Decoding JSON is just
json.loads.” Correction:json.loadshands back an untyped shape.Decode.decodeStringproduces your record or a structured error, so the rest of the program never meets an untyped value. - “
result { }is a special block that runs differently at runtime.” Correction: it desugars to the sameResultshort-circuit you would write by hand with nested matches.let!andreturnare sugar for bind and wrap. - “Units of measure add runtime overhead to every number.” Correction: units exist only during type checking and erase at lowering. The emitted Python is plain numbers.
- “A module changes how the code runs or performs.” Correction: an in-file module is purely
organizational namespacing. Members call each other by bare name inside, and you qualify them as
Module.memberfrom outside.
Timing
- 10 min: recap the unit so far, then frame boundary versus engine.
- 20 min: demo steps 1 to 2 (extern and JSON decoding).
- 15 min: demo steps 3 to 4 (computation expressions).
- 15 min: demo steps 5 to 6 (units and modules).
- 20 min: demo steps 7 to 8 (the capstone) or students start it themselves.
- 10 min: unit wrap-up and pointers to the full course and playground.
Answer keys: answer-keys.md