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 rather than 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 not documentation that hopes to stay true. It 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. That is the whole argument for mandatory signatures in one picture — 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 inside tax with 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.

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 are worth stating 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, which is chapter 10’s business; here it means a counter you increment inside a closure is not the counter outside it.

Exercise 4-1 (fingers · lupin) — Functions are values. Write compose so that compose(double, double) returns a function, and apply it to 10.

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 function exits, whichever way it exits. 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 is the part worth 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
    }
    total
}
fn main() -> !int {
    print("{audit("340\n275")}")
    0
}
$ lupin audit.lu
row done
row done
audit finished
615

Two rows, two “row done” lines: a defer inside a loop registers once per turn. And they all run at the function’s exit rather than at the end of each turn — defer is scoped to the function, which is what makes it a resource idiom rather than a block-local one. A defer that was never reached never registers, which is exercise 4-6’s business.

This is where files would close themselves. The samples above print instead, because they run under the reference interpreter, which has no filesystem by design; chapter 26 opens real files under the compiler, and the shape there is the shape here. 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]()
    rows.push("340")
    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. The rule is one sentence:

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 the common case.

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() { 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, applied at a function boundary rather than an assignment.

What the machine does. A borrow is an address. Passing rows to total passes the machine words that locate the list, not the elements — the cost is the same whether the list holds two rows or two million, which is why calling total(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 — which is not a coincidence, and not the whole language. 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?

Exercises 4-3 and 4-4 belong to this section’s material and are set in §7.4 instead, where the one word they need — a call-site mut — has been taught. They keep their numbers; Part 1 does not print them.