23. Keyword arguments at the boundary
Python functions lean on keyword arguments with defaults. int takes a base, open takes an
encoding, and requests.get takes a timeout. A Pyfun arrow is positional, so an extern needs a
way to say which keyword a value belongs to. A trailing (kw = value, …) on the target says it, and
the value comes in two kinds.
The first kind is a literal, fixed once at the declaration:
extern parseHex: string -> int = int(base = 16)
let raw = "ff"
raw |> parseHex |> print
255
parseHex reads hexadecimal and nothing else, because the 16 belongs to the declaration rather
than the call. The emitted Python puts the keyword back where Python wants it:
raw = "ff"
print(int(raw, base=16))
That suits a value which never changes. When it does change, ... in place of the literal takes the
value from the caller:
extern parseIn: string -> int -> int = int(base = ...)
let fromHex s = parseIn s 16
let fromBinary s = parseIn s 2
let a = "ff"
let b = "1011"
a |> fromHex |> print
b |> fromBinary |> print
255
11
One extern now covers every base. The type gained an argument for the slot to claim, and
parseIn : string -> int -> int is an ordinary curried arrow, so inference, effects, and the checker
treat it like any other function. What changes is where the argument lands in the emitted call:
def fromHex(s):
return int(s, base=16)
def fromBinary(s):
return int(s, base=2)
The binding rule reads the way a Python call reads. The target takes the leading arguments
positionally, and the slots take the trailing ones in the order the keywords are written. A pinned
literal claims no argument, so the two mix freely: builtins.open(mode = "rt", encoding = ...) takes
a path first and an encoding second. Slots also work on the instance-access targets from lesson 12,
where the receiver claims the first argument, as in = .write_text(encoding = ...).
Currying holds across the boundary. A slot extern applied to some of its arguments is a function awaiting the rest, like any other Pyfun function:
extern parseIn: string -> int -> int = int(base = ...)
let ff = parseIn "ff"
ff 16 |> print
255
The spelling comes from Python’s own stub files, where def get(url, timeout=...) marks a value the
signature declines to spell out. Reach for a pinned literal when every call wants the same value, and
for a slot when the caller decides.
Exercise
The program below declares two externs that differ only in the base each one pins. Replace them with a single extern whose base comes from the caller, and keep the output the same.
# Two externs that differ only in the base each one pins.
extern parseHex: string -> int = int(base = 16)
extern parseBinary: string -> int = int(base = 2)
let hex = "ff"
let binary = "1011"
hex |> parseHex |> print
binary |> parseBinary |> print
Expected output:
255
11
Show solution
extern parseIn: string -> int -> int = int(base = ...)
let fromHex s = parseIn s 16
let fromBinary s = parseIn s 2
let hex = "ff"
let binary = "1011"
hex |> fromHex |> print
binary |> fromBinary |> print
One declaration replaces both, and the base moves from the declaration to the call. The two named
helpers keep the call sites reading as pipelines, and either one can be passed around on its own,
because parseIn s is a function awaiting a base.