Skip to content

Commit 508a77f

Browse files
authored
docs: opaque types lesson (22, Going further track) (#14)
Worked examples (distinct ids on one underlying string, the checker rejecting a mix-up, the erased Python, an extern typed with the opaque type), a make-it-compile exercise with playground deep link, and the units-of-measure positioning note. SUMMARY + course-overview updated. All snippets and the deep link verified by docs/verify_lessons.py (22/22 lessons OK).
1 parent 99c7bb0 commit 508a77f

3 files changed

Lines changed: 151 additions & 2 deletions

File tree

docs/src/SUMMARY.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@
2929
- [Active patterns](learn/19-active-patterns.md)
3030
- [Build your own computation expression](learn/20-build-your-own-ce.md)
3131
- [Strings and numbers, in detail](learn/21-strings-and-numbers.md)
32+
- [Opaque types](learn/22-opaque-types.md)
3233

3334
# For educators
3435

docs/src/learn/22-opaque-types.md

Lines changed: 147 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,147 @@
1+
# 22. Opaque types
2+
3+
A user id and an email address can both live in a `string`, and a plain `string` will let you pass
4+
one where the other belongs. An opaque type gives a value of an existing type its own name, and the
5+
checker then keeps the two apart everywhere. You declare one with `opaque type`, wrap with the
6+
constructor of the same name, and unwrap with a single-case `match`.
7+
8+
```pyfun
9+
opaque type UserId = string
10+
opaque type Email = string
11+
12+
let uid = UserId "u-1001"
13+
let contact = Email "ana@example.org"
14+
15+
let describe u =
16+
match u:
17+
case UserId s: s
18+
19+
uid |> describe |> print
20+
```
21+
22+
```console
23+
u-1001
24+
```
25+
26+
`UserId` and `Email` are distinct types built on the same underlying `string`. The constructor
27+
`UserId : string -> UserId` wraps a value, and the pattern `case UserId s:` binds the underlying
28+
string back out. That one case makes the match exhaustive, because the type has exactly one shape.
29+
30+
Mixing the two up is where the checker steps in. Passing an `Email` to a function that takes a
31+
`UserId` is rejected before any Python is produced:
32+
33+
```pyfun
34+
opaque type UserId = string
35+
opaque type Email = string
36+
37+
let describe u =
38+
match u:
39+
case UserId s: s
40+
41+
let bad = describe (Email "ana@example.org")
42+
```
43+
44+
```console
45+
error: type mismatch: expected UserId, found Email
46+
```
47+
48+
The distinction exists only during type checking. Like units of measure, an opaque type erases at
49+
lowering, so the emitted Python is the plain underlying value with no wrapper class and no
50+
allocation:
51+
52+
```python
53+
uid = "u-1001"
54+
contact = "ana@example.org"
55+
def describe(u):
56+
match u:
57+
case s:
58+
return s
59+
print(describe(uid))
60+
```
61+
62+
The wrap compiled to nothing, and the pattern became a plain capture. This zero-cost story pays off
63+
at the Python boundary: because the running value is the underlying string, an `extern` can carry
64+
the opaque type in its signature, and the Python side receives exactly the `str` it expects.
65+
66+
```pyfun
67+
opaque type UserId = string
68+
69+
extern pure shout: UserId -> string = str.upper
70+
71+
let uid = UserId "u-1001"
72+
let loud = uid |> shout
73+
print loud
74+
```
75+
76+
```console
77+
U-1001
78+
```
79+
80+
The signature enforces the domain distinction on the Pyfun side, and `str.upper` runs on a plain
81+
string at runtime. An opaque type can also take parameters (`opaque type Tag a = List a`) and wrap
82+
any type, including lists and tuples. For numeric quantities with arithmetic, units of measure
83+
(lesson 14) remain the sharper tool, since they combine algebraically; opaque types cover ids,
84+
tokens, sanitized text, and every other value whose meaning outgrows its representation.
85+
86+
## Exercise
87+
88+
The program below reads an order id as a plain string, then hands it straight to a function that
89+
takes an `OrderId`. Run `pyfun check` to see the mismatch, then wrap the string at the call site so
90+
the program type-checks.
91+
92+
```pyfun
93+
opaque type OrderId = string
94+
95+
let orderLabel o =
96+
match o:
97+
case OrderId s: String.concat "order " s
98+
99+
# The id arrives as a plain string from the outside world.
100+
let raw = "o-9"
101+
102+
# This line does not type-check yet: orderLabel wants an OrderId.
103+
let label = orderLabel raw
104+
105+
print label
106+
```
107+
108+
The checker reports:
109+
110+
```console
111+
error: type mismatch: expected OrderId, found string
112+
--> 11:13
113+
|
114+
11 | let label = orderLabel raw
115+
| ^^^^^^^^^^^^^^
116+
```
117+
118+
Expected output:
119+
120+
```console
121+
order o-9
122+
```
123+
124+
[Open in the playground](https://simontreanor.github.io/Pyfun/playground/#code=b3BhcXVlIHR5cGUgT3JkZXJJZCA9IHN0cmluZwoKbGV0IG9yZGVyTGFiZWwgbyA9CiAgbWF0Y2ggbzoKICAgIGNhc2UgT3JkZXJJZCBzOiBTdHJpbmcuY29uY2F0ICJvcmRlciAiIHMKCiMgVGhlIGlkIGFycml2ZXMgYXMgYSBwbGFpbiBzdHJpbmcgZnJvbSB0aGUgb3V0c2lkZSB3b3JsZC4KbGV0IHJhdyA9ICJvLTkiCgojIFRoaXMgbGluZSBkb2VzIG5vdCB0eXBlLWNoZWNrIHlldDogb3JkZXJMYWJlbCB3YW50cyBhbiBPcmRlcklkLgpsZXQgbGFiZWwgPSBvcmRlckxhYmVsIHJhdwoKcHJpbnQgbGFiZWwK)
125+
126+
<details>
127+
<summary>Show solution</summary>
128+
129+
```pyfun
130+
opaque type OrderId = string
131+
132+
let orderLabel o =
133+
match o:
134+
case OrderId s: String.concat "order " s
135+
136+
# The id arrives as a plain string from the outside world.
137+
let raw = "o-9"
138+
139+
let label = orderLabel (OrderId raw)
140+
141+
print label
142+
```
143+
144+
Wrapping the string with `OrderId` at the boundary is the whole fix, and the whole idiom: raw data
145+
enters, gets named once, and every function past that point can trust what it holds. The wrap costs
146+
nothing at runtime.
147+
</details>

docs/src/learn/README.md

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -45,5 +45,6 @@ Lessons 12 to 16 reach outward: calling Python libraries, computation expression
4545
measure, multi-file projects, and a capstone that puts everything together.
4646

4747
After the capstone, a **Going further** set digs into topics the core path skips: recursion,
48-
async, active patterns, building your own computation expression, and the fine print of strings
49-
and numbers. Take them in any order once you have finished the core lessons they build on.
48+
async, active patterns, building your own computation expression, the fine print of strings and
49+
numbers, and opaque types. Take them in any order once you have finished the core lessons they
50+
build on.

0 commit comments

Comments
 (0)