4. Functions
The receipt has been one long main for three chapters. It has also been
doing the same thing twice: chapter 2 wrote a comma scan, then wrote it
again to measure a column. Code that appears twice is a function that has
not been named yet.
fn comma(row: str) -> int {
var i = 0
while row[i..i + 1] != "," { i += 1 }
i
}
fn name_of(row: str) -> str { row[..comma(row)] }
fn cents_of(row: str) -> int { row[comma(row) + 1..].to_int() else 0 }
fn main() -> !int {
let rows = """
espresso,340
pastry,275
tip,100
"""
var total = 0
for row in rows.lines() {
total += cents_of(row)
print("{name_of(row):<10}{cents_of(row):>5}")
}
print("{"total":<10}{total:>5}")
0
}
$ lupin fields.lu
espresso 340
pastry 275
tip 100
total 715
The loop body is now two lines that say what they mean, and the byte arithmetic lives in one place where it can be got right once. Nothing in those three signatures is ceremony: every word in them is load-bearing, and this chapter is about which word does what.
4.1 Signatures are the contract
A function declares its parameter types and its return type. It declares nothing about its body:
fn widest(rows: str) -> int {
var width = 0
for row in rows.lines() {
var i = 0
while row[i..i + 1] != "," { i += 1 }
if i > width { width = i }
}
width
}
fn main() -> !int {
let rows = """
espresso,340
pastry,275
tip,100
"""
print("{widest(rows)}")
0
}
$ lupin widest.lu
8
width is an int because 0 is, row is a str because lines()
yields those, i is an int because it starts at zero and gets one
added to it. None of that is written down, and writing it down would add
no information: the compiler works it out from the same evidence you
did. The rule is that inference stops at the boundary: inside a body it
does everything, and at a signature it does nothing.
That line is drawn where it is on purpose. A signature is the only part of a function its callers read, and a function whose parameter types were inferred from its body would change its interface every time somebody edited an expression inside it. So the boundary is spelled, and leaving it out is an error, not an invitation to guess:
fn cents_of(row) -> int { 0 }
fn main() -> !int {
print("{cents_of("tip,100")}")
0
}
error[E0201]: expected `:` and a type after the parameter name
--> ./s3.lu:4:16
|
4 | fn cents_of(row) -> int { 0 }
| ^
|
Once written, the signature is documentation the compiler enforces, at every call:
fn tax(cents: int) -> int { cents + cents / 20 }
fn main() -> !int {
print("{tax("340")}")
0
}
error[E0401]: this is `str`, but `tax` needs its 1st argument to be `int`
--> ./s4.lu:6:17
|
4 | fn tax(cents: int) -> int { cents + cents / 20 }
| ----- the parameter is declared here
5 | fn main() -> !int {
6 | print("{tax("340")}")
| ^^^^^ found `str` here
|
= note: expected `int`, found `str`
The diagnostic shows both ends: where the promise was made and where it was broken. The error names a disagreement between two places, which is possible only because both places said something.
Coming from Python: the annotations look like Python’s type hints and behave nothing like them. A hint is advice for a separate tool; a wolf signature is checked by the compiler that produces the program, so
tax("340")does not run and then fail somewhere insidetaxwith a message about/. The part Python users tend to miss on the way in is the other half of the trade: because bodies are inferred, the annotation burden is a per-function constant rather than per-expression. You write types where a reader needs them and nowhere else.
Exercise 4-5 (comprehension · lupin). Euclid’s algorithm, in the expression style:
fn gcd(a: int, b: int) -> int {
if b == 0 { a } else { gcd(b, a % b) }
}
Trace gcd(1071, 462) on paper (write down every (a, b) pair the
recursion visits) and state the result before running it.
Exercise 4-10 (fingers · lupin). Write rtrim: trailing blanks
and tabs gone, everything else kept. Walk backward from the end with
byte slices and return one slice of the original. Print each result in
brackets so the trimming is visible. What does your function return
for a line that is all blanks, and did that case cost you code?
4.2 Functions as values
A function value is written fn(params) expression, with the types left
off, because at the point where you write one the compiler already knows
what it is being used as:
fn adjusted(cents: int, f: fn(int) -> int) -> int { f(cents) }
fn main() -> !int {
let discount = fn(c) c - c / 10
let tax = fn(c) c + c / 20
print("{adjusted(340, discount)} {adjusted(340, tax)}")
let both = fn(c) tax(discount(c))
print("{adjusted(340, both)}")
0
}
$ lupin policy.lu
306 357
321
adjusted takes a function as its second parameter, and the type of
that parameter is written the way the function is called: fn(int) -> int takes an int and gives an int. both composes two of them with no
apparatus at all. A function value is an ordinary value, so it goes in
a let, into a call, and out of one:
fn rounder(to: int) -> fn(int) -> int {
fn(c) (c + to / 2) / to * to
}
fn main() -> !int {
let nickel = rounder(5)
let dollar = rounder(100)
print("{nickel(273)} {dollar(273)} {dollar(nickel(273))}")
0
}
$ lupin rounder.lu
275 300 300
rounder returns a closure: a function value that carries part of its
environment with it. The to inside the returned body is rounder’s
parameter, and it is still 5 when nickel is called three lines later,
long after rounder has returned. A closure whose body needs a block
takes one (fn(c) { … }, with the value as the last expression), and
everything §3.2 said about blocks applies inside it.
Two limits, while the form is in front of you. A
function value takes no signature of its own: writing
fn(c: int) -> int { … } is a parse error, because the form is defined
as parameters and an expression, and the types come from the context. And
a closure captures a value, never a place: a var read inside a closure
is the value the var held when the closure was made, and a write inside
the closure lands on the closure’s own copy. That rule is what makes a
closure safe to hand to a task (chapter 10’s subject); here it
means a counter you increment inside a closure is not the counter
outside it.
One more thing the form insists on, and it insists on it everywhere a list is written. Parameters are separated by commas, and the comma is not decoration:
fn apply(a: int, b: int, f: fn(int, int) -> int) -> int { f(a, b) }
fn main() -> !int {
let add = fn(x y) x + y
print("{apply(2, 3, add)}")
0
}
error[E0201]: expected `,` between the closure's parameters
--> ./s8.lu:6:20
|
6 | let add = fn(x y) x + y
| ^
|
= note: the separating comma is required between closure parameters ([gram.expr.closure]:
closure_param (',' closure_param)* ','?)
help: add the comma
|
6 | let add = fn(x, y) x + y
|
The compiler quotes the grammar production it is holding you to, and
then writes the repair out in the shape of the line you wrote;
wolf fix --apply applies it without your typing anything. One
sentence governs the whole language here, not only closures: a struct
literal’s fields, a pattern’s members and the .. that follows them,
and an inline C block’s capture list all take the same separator, and
all decline the same way. Where this book prints a list of anything the
commas are in it, which is worth noticing precisely because they used
to be optional in one of the two implementations and are not any more.
Exercise 4-1 (fingers · lupin). Functions are values. Write
compose so that compose(double, double) returns a function, and apply
it to 10.
Exercise 4-9 (extension · lupin). Write clamp_to(lo, hi), a
function that returns the clamping function for that range, and use
clamp_to(0, 100) to sanitize a list of parsed percentages. The
closure captures lo and hi by value. What would break, and what
would not, if it captured them by place?
4.3 defer
Cleanup belongs next to the thing that needs cleaning up, not at the bottom of the function where you will forget it during a refactor:
fn till() -> int {
print("till opened")
defer print("till closed")
print("counting")
715
}
fn main() -> !int {
print("total {till()}")
0
}
$ lupin till.lu
till opened
counting
till closed
total 715
defer takes an expression (or a block) and runs it when the enclosing
scope exits, whichever way it exits; here that scope is the function’s
own body. The order of the output is the point: “till closed” is
written after “counting” and before the caller sees the value,
because the deferred work happens at the exit, not where it was
written.
Several defers unwind in reverse:
fn main() -> !int {
defer print("3: till closed")
defer print("2: drawer locked")
defer print("1: lights off")
print("0: working")
0
}
$ lupin closing.lu
0: working
1: lights off
2: drawer locked
3: till closed
defer is a stack, because teardown has to unwind what setup wound: the
drawer was locked while the till was open, so it gets unlocked first.
Write acquisitions in order, and the releases arrange themselves.
The stack is built at runtime, not at compile time, and that deserves a program of its own:
fn audit(rows: str) -> int {
defer print("audit finished")
var total = 0
for row in rows.lines() {
defer print("row done")
total += row.to_int() else 0
print("running {total}")
}
total
}
fn main() -> !int {
print("{audit("340\n275")}")
0
}
$ lupin audit.lu
running 340
row done
running 615
row done
audit finished
615
Two rows, two “row done” lines, and each one lands before the next turn
begins. A defer inside a loop registers once per turn and runs when
that turn’s block exits: the scope is what times it, not the frame. A
loop body is a scope and it exits every time around, so the file opened
at the top of one turn closes before the next turn opens another. That
is what makes defer a resource idiom rather than a way to pile
cleanups up until the function is done with them. “audit finished” is
last because its scope is the whole function body, which exits once. A
defer that was never reached never registers (exercise 4-6 turns on
that).
“Whichever way it exits” has one exception, and it is worth meeting
before you write a defer you are counting on. A trap — §3.3’s, the
fault of a defined execution — is not a way out of a scope; it is the
end of the program. Nothing pending runs:
fn main() -> !int {
defer print("till closed")
{
defer print("drawer locked")
print("counting")
}
let drawer = 0
assert(drawer > 0)
print("unreachable")
0
}
$ wolf run closing.lu
counting
drawer locked
wolf-trap: assert
at ./closing.lu:8:5
$ echo $?
134
The inner block exits normally, so its defer runs, on time and in
place. The outer one never does. The assertion fails and the process is
over where it stands: a trap runs no defer and no errdefer,
anywhere, and everything still pending when it fires is abandoned. A
trap is not an error value making its way out through your cleanup
code; it is the machine reporting that the program was wrong, and
stopping. Appendix B is where that rule is written down with the clause
behind it, alongside the twelve kinds and what each implementation
exits with. §8.9 meets the same rule one boundary down, where a trap
inside a proc abandons the defers below the proc boundary and leaves
the ones above it alone.
Both machines do this now, and this printing runs the program on both.
The compiler was the letter first, on all three of its lanes, and the
reference interpreter ran the outer defer on its way out — a till closed line between drawer locked and its trap report. That was a
recorded divergence with an issue number rather than a second reading
of the rule, and the rule is what changed to meet it: at this
printing’s pins the program above prints counting and drawer locked and nothing else under either machine, and each one reports the
trap in its own documented voice (Appendix B) — the block above is the
compiler’s, which exits 134, and the interpreter names the same
assert and exits 3.
This is where files would close themselves. The samples above print
instead, because these are small programs about the ordering and a
print is the shortest thing that shows it; chapter 26 opens real
files, and the shape there is the shape here. That sentence used to
carry a second clause — that the reference interpreter had no
filesystem — and it retired at lupin 0.1.36, which reads and writes
real files like the compiler does. The error path (a
cleanup that should run only when things went wrong) is errdefer,
which waits for chapter 6 because it needs an error to defer on.
Exercise 4-2 (comprehension · lupin). Predict the order of the three lines:
fn main() -> !int {
defer print("first registered")
defer print("second registered")
print("body")
0
}
Exercise 4-6 (comprehension · lupin). A defer is registered
when execution reaches it. Predict all five output lines, in order:
fn work(n: int) -> int {
defer print("one")
if n == 0 { return 10 }
defer print("two")
20
}
fn main() -> !int {
print("{work(0)}")
print("{work(1)}")
0
}
4.4 Borrow by default
Count the sigils in this program:
fn total(rows: List[str]) -> int {
var t = 0
for row in rows { t += row.to_int() else 0 }
t
}
fn widest(rows: List[str]) -> int {
var w = 0
for row in rows { if row.len > w { w = row.len } }
w
}
fn main() -> !int {
var rows = List[str]()
(mut rows).push("340")
(mut rows).push("2750")
print("{total(rows)} {widest(rows)} {rows.len}")
0
}
$ lupin twice.lu
3090 4 2
There are none, and one list was read by two functions and then read
again by main. Compare that with §3.1, where handing a list to a let
ended the original’s usefulness.
A parameter borrows; a return moves.
Passing rows to total lends it for the duration of the call. The
function can read it, cannot keep it, and gives it back by returning,
so the next line still has a list. That is why the two calls compose and
why nothing had to be spelled: reading somebody else’s value for the
length of a call is the overwhelmingly common case, and wolf spends no
syntax on it.
Returning is the other direction. A function that builds a value hands it out, exactly as an assignment hands one over:
fn amounts(text: str) -> List[int] {
var out = List[int]()
for row in text.lines() { (mut out).push(row.to_int() else 0) }
out
}
fn main() -> !int {
let cents = amounts("340\n275\n100")
print("{cents.len} rows, first {cents[0]}")
0
}
$ lupin amounts.lu
3 rows, first 340
out is local to amounts and would be finished when the function
returns, except that the last expression hands it to the caller instead.
cents owns it now. No copy was made and no annotation was written:
“returns move” is the same rule as §3.1’s, now applied at a function
boundary.
What the machine does. A borrow is an address. Passing
rowstototalpasses the machine words that locate the list; the cost is the same whether the list holds two rows or two million, which is why callingtotal(rows)twice in one line is not a performance question. A return is a move, and a move is a copy of those same few words plus a promise that the source will not be read again (the promise §3.1’s diagnostic enforces). So neither direction copies the elements, and the thing the compiler is actually tracking is not data movement, it is who is allowed to read this next.
One thing this chapter has not shown is a function that changes its
argument. Both total and widest only read, and every sample in Part 1
is like them, by design.
Mutation through a parameter is written at both ends in wolf, at the
definition and at every call, and both the mechanism and the argument
for it are §7.4’s. Until then: parameters borrow, returns move, and Part
1 does not need a third rule.
Exercise 4-7 (extension · lupin). Build day_of_year(month, day, leap) from two functions: days_in(month, leap) as one match, and a
loop that sums the months before yours. Verify: March 1st is day 60 in a
common year. Which date is day 60 in a leap year, and where in your code
did that difference come from?
Exercise 4-8 (fingers · lupin). The Collatz walk: halve an even
number, triple-and-add-one an odd one, count the steps to reach 1.
Write steps(n) and print a two-column table for 1 through 10. Which
starting point under 11 takes the longest, and is the answer where you
expected it in the table?
Exercise 4-11 (extension · lupin). Zeller’s congruence names the
weekday of any date in four lines of arithmetic — January and February
count as months 13 and 14 of the year before. Write weekday(y, m, d)
returning the day’s name from one match, and check it against three
dates whose weekday is a matter of record. Why does the month shift
exist: what property of the calendar is it buying back?
Exercises 4-3 and 4-4 moved to chapter 7, where the one word they
need (a call-site mut) is taught: 4-3 is 7-6 there, and 4-4 became
part of 7-9. The numbers are not reused.