Lesson 11 of 25 · Functions

defer and function modifiers

defer keeps cleanup next to the code that needs it, so it cannot be forgotten on an early return. The function modifiers tell the compiler how a function behaves, and some of them it checks for you.

defer

A statement written after defer does not run where it stands. It runs when the surrounding block ends. With several defers, the last one runs first, the way you undo steps in reverse order. The commonest use is releasing something right beside the line that acquired it.

defer.salam
func work():
    println "starting work"
    defer println "first cleanup"
    defer println "second cleanup"
    println "doing the work"
end

func main:
    work()
    println "after the work"
end
Outputstarting work doing the work second cleanup first cleanup after the work

Every way out

A deferred statement runs whichever way the block is left: at its end, at a ret in the middle, or at a break out of a loop. That is the whole point. A function with several ret statements needs one defer, not a copy of the cleanup before each ret.

defer-return.salam
func find(items: int[], wanted: int): int:
    defer println "search for", wanted, "finished"
    each i, item in items:
        if item == wanted:
            ret i
        end
    end
    ret -1
end

func main:
    codes := [404, 200, 500]
    println "position:", find(codes, 200)
    println "position:", find(codes, 301)
end
Outputsearch for 200 finished position: 1 search for 301 finished position: -1

The deferred line runs after the result has been worked out but before the caller gets it, so it is printed before position:. The same holds in main: a ret 1 still runs main's defers, which is why os.Exit is not allowed there.

defer belongs to its block

A defer runs at the end of the block it is written in, not at the end of the function. Inside a loop that means at the end of every round, and inside an if at the end of that branch.

defer-blocks.salam
func main:
    repeat 3 in day:
        defer println "  close day", day
        println "open day", day
    end
    late := 2
    if late > 0:
        defer println "  report sent"
        println "late orders:", late
    end
    println "done"
end
Outputopen day 0 close day 0 open day 1 close day 1 open day 2 close day 2 late orders: 2 report sent done

So something acquired inside a loop body and deferred there is released before the next round starts, which is usually exactly what you want.

When the values are read

A deferred statement reads its variables when it runs, not when the defer line is reached. If a variable changes in between, the defer sees the new value. To keep the value from the moment of the defer, copy it into another variable first.

defer-late.salam
func main:
    mut stage := "start"
    defer println "deferred sees:", stage
    saved := stage
    defer println "saved copy:", saved
    stage = "finish"
    println "stage now:", stage
end
Outputstage now: finish saved copy: start deferred sees: finish

Freeing collections

A Vector or a HashMap takes memory that you give back with free(). Write defer x.free() on the line right after creating it, and every path out of the function frees it, including the early ret 0 here.

defer-free.salam
func average reading(raw: int[]): int:
    mut valid := Vector {} as Vector<int>
    defer valid.free()
    each r in raw:
        if r >= 0:
            valid.push(r)
        end
    end
    if valid.len() == 0:
        ret 0
    end
    mut sum := 0
    each v in valid:
        sum += v
    end
    ret sum / valid.len()
end

func main:
    println average reading([21, -1, 23, 25, -1])
    println average reading([-1, -1])
end
Output23 0

The collections lesson covers Vector and HashMap, and the memory lesson explains what needs freeing and why.

Closing files

The same pattern closes files. Check that the open worked, then defer the close straight away; the code that follows may return early without leaving the file open. Here main also defers deleting the temporary file, so it is gone when the program ends.

defer-file.salam
import os

func save report(path: str, total: int): bool:
    f := os.Open(path, "w")
    if f == null:
        ret false
    end
    defer f.close()
    f.write("total: " + total + "\n")
    f.write("status: paid\n")
    ret true
end

func main:
    path := os.TempDir() + "/salam-report.txt"
    if not save report(path, 245):
        printerrln "cannot write", path
        ret 1
    end
    defer os.Remove(path)
    print os.ReadFile(path)
end
Outputtotal: 245 status: paid

A defer may call a function that returns a value, as os.Remove does; the value is simply dropped. There is no block form defer: ... end. When the cleanup takes several steps, put them in a function and defer the call.

Function modifiers

These words go before func and say something about the function. Some are promises the compiler checks, others are hints:

ModifierMeaningWhat the compiler does
pureNo side effects: the result depends only on the argumentsRejects printing, writing a global or calling an impure function inside it
inlinePlace the body at the call siteTakes it as a hint, useful for small functions called very often
noinlineKeep it a real callTakes it as a hint, handy when profiling or debugging
noretThe function never returns, for example it ends the programTreats code after a call to it as unreachable
deprecatedThe function is obsoleteWarns at every call (W013)
pubVisible outside its packageSee the packages lesson
fn-mods.salam
pure func net price(price: int, discount: int): int:
    ret price - price * discount / 100
end

inline func square(n: int): int:
    ret n * n
end

noinline func receipt line(name: str, amount: int): str:
    ret name + ": " + amount
end

func main:
    println net price(200, 15), square(9)
    println receipt line("tea", net price(90, 10))
end
Output170 81 tea: 81

Add a println to net price and the build stops with "'pure' function 'net price' cannot call 'println': it has side effects". That check is what makes pure worth writing: a pure function is safe to call anywhere, any number of times.

fn-noret.salam
import os

noret func fail(message: str):
    printerrln "error:", message
    os.Exit(2)
end

deprecated func old total(a: int, b: int): int:
    ret a + b
end

func check age(age: int): int:
    if age < 0 or age > 150:
        fail("impossible age " + age)
    end
    ret age
end

func main:
    println check age(34)
    println old total(2, 3)
end
Output34 5

Called with an age of -3, check age prints "error: impossible age -3" and the program exits with code 2. Because fail is noret, the compiler knows the call never comes back, and a statement written right after it is rejected as unreachable (E070). It does not check the body, though: a noret function that does return crashes the program, so make sure it always ends in os.Exit or another call that never returns. Calling old total builds and runs, with a W013 warning.

Rules of thumb

  • Write the defer on the line right after you acquire the thing it releases.
  • Defers run in reverse order, at the end of their own block.
  • A deferred statement sees the values its variables have when it runs.
  • Mark a function pure when it is, and let the compiler keep it that way.
  • Use noret only for functions that really end the program.