Skip to content

Commit 412cd3b

Browse files
authored
docs: document extern keyword arguments (#22)
* examples/interop: document extern keyword arguments The cookbook's pattern catalogue covered instance access, class targets and extern import, but never keyword arguments on a target. One bullet now covers both forms: a literal pinned at the declaration, and a `...` slot taking its value from the caller. * docs: lesson 23, keyword arguments at the boundary A Going further lesson covering both forms of extern keyword argument: a literal pinned at the declaration, and a `...` slot taking its value from the caller. Walks the binding rule, the emitted Python, and the fact that currying holds across the boundary; the exercise collapses two rigid externs into one slot extern. Verified by docs/verify_lessons.py (deep link, solution output, and every worked-example block).
1 parent c1d5f1f commit 412cd3b

4 files changed

Lines changed: 145 additions & 2 deletions

File tree

docs/src/SUMMARY.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@
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)
3232
- [Opaque types](learn/22-opaque-types.md)
33+
- [Keyword arguments at the boundary](learn/23-keyword-arguments.md)
3334

3435
# For educators
3536

Lines changed: 132 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,132 @@
1+
# 23. Keyword arguments at the boundary
2+
3+
Python functions lean on keyword arguments with defaults. `int` takes a `base`, `open` takes an
4+
`encoding`, and `requests.get` takes a `timeout`. A Pyfun arrow is positional, so an `extern` needs a
5+
way to say which keyword a value belongs to. A trailing `(kw = value, …)` on the target says it, and
6+
the value comes in two kinds.
7+
8+
The first kind is a literal, fixed once at the declaration:
9+
10+
```pyfun
11+
extern parseHex: string -> int = int(base = 16)
12+
13+
let raw = "ff"
14+
15+
raw |> parseHex |> print
16+
```
17+
18+
```console
19+
255
20+
```
21+
22+
`parseHex` reads hexadecimal and nothing else, because the `16` belongs to the declaration rather
23+
than the call. The emitted Python puts the keyword back where Python wants it:
24+
25+
```python
26+
raw = "ff"
27+
print(int(raw, base=16))
28+
```
29+
30+
That suits a value which never changes. When it does change, `...` in place of the literal takes the
31+
value from the caller:
32+
33+
```pyfun
34+
extern parseIn: string -> int -> int = int(base = ...)
35+
36+
let fromHex s = parseIn s 16
37+
let fromBinary s = parseIn s 2
38+
39+
let a = "ff"
40+
let b = "1011"
41+
42+
a |> fromHex |> print
43+
b |> fromBinary |> print
44+
```
45+
46+
```console
47+
255
48+
11
49+
```
50+
51+
One extern now covers every base. The type gained an argument for the slot to claim, and
52+
`parseIn : string -> int -> int` is an ordinary curried arrow, so inference, effects, and the checker
53+
treat it like any other function. What changes is where the argument lands in the emitted call:
54+
55+
```python
56+
def fromHex(s):
57+
return int(s, base=16)
58+
def fromBinary(s):
59+
return int(s, base=2)
60+
```
61+
62+
The binding rule reads the way a Python call reads. The target takes the leading arguments
63+
positionally, and the slots take the trailing ones in the order the keywords are written. A pinned
64+
literal claims no argument, so the two mix freely: `builtins.open(mode = "rt", encoding = ...)` takes
65+
a path first and an encoding second. Slots also work on the instance-access targets from lesson 12,
66+
where the receiver claims the first argument, as in `= .write_text(encoding = ...)`.
67+
68+
Currying holds across the boundary. A slot extern applied to some of its arguments is a function
69+
awaiting the rest, like any other Pyfun function:
70+
71+
```pyfun
72+
extern parseIn: string -> int -> int = int(base = ...)
73+
74+
let ff = parseIn "ff"
75+
76+
ff 16 |> print
77+
```
78+
79+
```console
80+
255
81+
```
82+
83+
The spelling comes from Python's own stub files, where `def get(url, timeout=...)` marks a value the
84+
signature declines to spell out. Reach for a pinned literal when every call wants the same value, and
85+
for a slot when the caller decides.
86+
87+
## Exercise
88+
89+
The program below declares two externs that differ only in the base each one pins. Replace them with
90+
a single extern whose base comes from the caller, and keep the output the same.
91+
92+
```pyfun
93+
# Two externs that differ only in the base each one pins.
94+
extern parseHex: string -> int = int(base = 16)
95+
extern parseBinary: string -> int = int(base = 2)
96+
97+
let hex = "ff"
98+
let binary = "1011"
99+
100+
hex |> parseHex |> print
101+
binary |> parseBinary |> print
102+
```
103+
104+
Expected output:
105+
106+
```console
107+
255
108+
11
109+
```
110+
111+
[Open in the playground](https://simontreanor.github.io/Pyfun/playground/#code=IyBUd28gZXh0ZXJucyB0aGF0IGRpZmZlciBvbmx5IGluIHRoZSBiYXNlIGVhY2ggb25lIHBpbnMuCmV4dGVybiBwYXJzZUhleDogc3RyaW5nIC0-IGludCA9IGludChiYXNlID0gMTYpCmV4dGVybiBwYXJzZUJpbmFyeTogc3RyaW5nIC0-IGludCA9IGludChiYXNlID0gMikKCmxldCBoZXggPSAiZmYiCmxldCBiaW5hcnkgPSAiMTAxMSIKCmhleCB8PiBwYXJzZUhleCB8PiBwcmludApiaW5hcnkgfD4gcGFyc2VCaW5hcnkgfD4gcHJpbnQK)
112+
113+
<details>
114+
<summary>Show solution</summary>
115+
116+
```pyfun
117+
extern parseIn: string -> int -> int = int(base = ...)
118+
119+
let fromHex s = parseIn s 16
120+
let fromBinary s = parseIn s 2
121+
122+
let hex = "ff"
123+
let binary = "1011"
124+
125+
hex |> fromHex |> print
126+
binary |> fromBinary |> print
127+
```
128+
129+
One declaration replaces both, and the base moves from the declaration to the call. The two named
130+
helpers keep the call sites reading as pipelines, and either one can be passed around on its own,
131+
because `parseIn s` is a function awaiting a base.
132+
</details>

docs/src/learn/README.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -46,5 +46,5 @@ 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,
4848
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.
49+
numbers, opaque types, and keyword arguments at the Python boundary. Take them in any order once
50+
you have finished the core lessons they build on.

examples/interop/README.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,16 @@ The honest headline is therefore **not** "rewrite the popular libraries in Pyfun
7878
wrapper is needed. And where a library defines its API on `+`/`-` (as `datetime` does),
7979
Python's `operator` module exposes every operator as a plain function: `= operator.add`
8080
is a ready-made extern target (`datetime.pyfun`).
81+
- **Keyword arguments on the target.** A trailing `(kw = value, …)` puts Python keyword
82+
arguments on every emitted call, reaching keyword-only and deep-positional parameters
83+
without the positional fillers needed to skip past them. A literal is pinned at the
84+
declaration (`extern openText : string -> a = builtins.open(mode = "rt")`), and `...`
85+
takes the value from the caller, so one extern covers every call shape instead of one per
86+
fixed value: `extern parseInt : string -> int -> int = int(base = ...)` lowers
87+
`parseInt "ff" 16` to `int("ff", base=16)`. Pinned literals consume no argument, so the
88+
two mix (`= builtins.open(mode = "rt", encoding = ...)`), and both work on a receiver
89+
method (`= .write_text(encoding = ...)`). A partially applied slot extern keeps working:
90+
`parseInt "ff"` is a function awaiting the base.
8191
- **`extern import` when the heuristic can't see the module.** A dotted target's module is
8292
guessed by its lowercase prefix, which mis-reads a lowercase *class* (or value attribute)
8393
as a submodule. Declare it explicitly — Python's own import statement, `as` and all:

0 commit comments

Comments
 (0)