Defer return
return and defer meet at the end of a function, and the order between them is fixed. return value; works out its value first and keeps it; only then do the deferred statements run. Whatever they change afterwards, the caller still receives the kept value.
That sounds like a detail, but it makes a useful pattern possible: "hand out the current value, then move on" becomes a two-line function, with no temporary variable to hold the old value.
The order at the end of a function
Countdown registers two deferred statements and then returns its local:
func Countdown() -> int32 {
var remaining: int32 = 3;
defer PrintLine(" deferred code sees remaining = {}", remaining);
defer remaining = 0;
return remaining;
}
Step by step, as the function ends:
flowchart LR
r["return remaining<br/>works out 3<br/>and keeps it"] --> d1["defer remaining = 0<br/>(registered last, runs first)"]
d1 --> d2["defer PrintLine(…)<br/>prints remaining = 0"]
d2 --> c["the caller<br/>receives 3"]countdown:
deferred code sees remaining = 0
returned 3
Two things are worth noticing in that output.
The PrintLine shows 0, not 3. A deferred statement is evaluated when it runs, not when it is registered — so it reads remaining after the reset has happened. And the reset did happen: the local really is 0. It simply makes no difference to the caller, who gets the 3 that return had already kept.
Hand out, then advance
A ticket dispenser shows a number. Taking a ticket gives you that number, and the display moves on to the next:
struct Dispenser {
next: int32;
}
extend Dispenser {
// Returns the current ticket, then advances the dispenser.
func Take(self: &var Dispenser) -> int32 {
defer self.next += 1;
return self.next;
}
}
return self.next keeps the current number; the deferred self.next += 1 then advances the dispenser. Without defer, the same method needs a temporary:
let ticket = self.next;
self.next += 1;
return ticket;
Both are correct. The defer version says what the method is for in its last line — return the current ticket — and puts the bookkeeping on the line before it.
var dispenser = Dispenser { next: 1 };
let first = dispenser.Take();
let second = dispenser.Take();
first is 1, second is 2, and the dispenser now shows 3. The method changes self, so it takes &var Dispenser, as in Mutating method.
| Statement | When it is evaluated | What the caller sees |
|---|---|---|
return self.next; | first — its value is kept | the kept value |
defer self.next += 1; | after the return value is kept | the change, on its next call |
The program
The whole lesson is one package in the Examples repository. Its comments explain every step.
// `return` and `defer` meet at the end of a function, and the order between them is fixed:
// `return value;` works out its value first and keeps it, and only then do the deferred
// statements run. Whatever they change afterwards, the caller still receives the kept value.
//
// That makes "hand out the current value, then move on" a two-line function. A ticket dispenser
// returns the number it is showing and advances to the next one, without a temporary variable to
// hold the old number.
import Io::PrintLine;
// Both deferred statements run after the return value is kept. In reverse order, so the reset
// runs first and the `PrintLine` then shows that the local really changed. The caller gets 3.
func Countdown() -> int32 {
var remaining: int32 = 3;
defer PrintLine(" deferred code sees remaining = {}", remaining);
defer remaining = 0;
return remaining;
}
struct Dispenser {
next: int32;
}
extend Dispenser {
// Returns the current ticket, then advances the dispenser.
func Take(self: &var Dispenser) -> int32 {
defer self.next += 1;
return self.next;
}
}
func Main() -> int {
PrintLine("countdown:");
let result = Countdown();
PrintLine(" returned {}", result);
PrintLine("tickets:");
var dispenser = Dispenser { next: 1 };
let first = dispenser.Take();
let second = dispenser.Take();
PrintLine(" took {} and {}, now showing {}", first, second, dispenser.next);
return 0;
}
Run it
cd Examples/Ownership/DeferReturn
rux run
countdown:
deferred code sees remaining = 0
returned 3
tickets:
took 1 and 2, now showing 3
Common mistakes
defer remaining = 0; does reset the local, but Countdown() still returns 3. The return value was kept before any deferred statement ran. If the caller must see the change, make it before the return.defer PrintLine("{}", remaining); does not capture remaining at the defer line. It reads the variable when it runs, at the end of the function, after everything else has changed it.With
func Take(self: &Dispenser), the deferred self.next += 1; fails with error: cannot modify data through immutable reference '&Dispenser'. Deferred code is checked like any other code in the function: changing self needs &var.Try it yourself
- Swap the two
deferlines inCountdown. What does the deferredPrintLineshow now, and does the returned value change? - Add a method
Peek(self: &Dispenser) -> int32that returns the next ticket without taking it, and print it between the twoTakecalls. - Write
func Next(counter: &var int32) -> int32that returns the counter's current value and then adds 1 to it, usingdefer. Call it twice on avarthat starts at 5.
Learn more
- Defer — registering cleanup, and the reverse order
- Mutating method —
self: &var T - Return — returning a value from a function