Reference / Closures and iterators
Closures and iterators
A closure is a function without a name. A program writes it where it needs it, and the closure can use the names around it.
fn apply_twice(f: (i64) -> i64, x: i64) -> i64 {
f(f(x))
}
fn main() {
let step = 3
print(apply_twice(x => x + step, 1)) // 7
}
A function becomes a value that a program can pass, store and return. An iterator produces the elements of a sequence one at a time, and for loops over it.
Function types In development
A function type is written (T1, T2) -> R. It lists the types of the parameters and the type of the result. () -> R takes nothing. A function that returns no value has the result type unit.
fn add(a: i64, b: i64) -> i64 {
a + b
}
fn adder(n: i64) -> (i64) -> i64 {
x => x + n
}
fn main() {
let ops: [(i64, i64) -> i64] = [add, (a, b) => a * b]
let plus = ops[0]
let times = ops[1]
print(plus(2, 3)) // 5
print(times(2, 3)) // 6
let add5 = adder(5)
print(add5(1)) // 6
}
The rules:
- A named function is a value of its function type.
addhas the type(i64, i64) -> i64. - A function type goes anywhere a type goes. It can be the type of a parameter, a local, a field, a payload, an array element or a type argument. It can be a return type or an optional.
->groups to the right.(i64) -> (i64) -> i64takes ani64and returns a function. An optional function type needs parentheses:(() -> unit)?.- A function type has no labels. A named function takes its arguments by position when a call goes through a value.
unitis a type name that a program can write.-> uniton a function declaration means the same as no return type.- Function types match exactly. A
(i64) -> i64does not convert to a(i64) -> i64?. A closure fits, becausesisuchecks its body against the expected type. - For a named function that does not fit, wrap it in a closure:
x => f(x). - A generic named function used as a value takes its type arguments from the expected type.
- A function value has no
==.f == gis an error.
class Button {
mut on_click: (() -> unit)?
}
enum Command {
Run(() -> unit)
Quit
}
fn hello() {
print("hello")
}
fn main() {
let b = Button(on_click: hello)
if let f = b.on_click {
f() // hello
}
let c = Command.Run(hello)
if let Command.Run(f) = c {
f() // hello
}
}
Calling a function value In development
f(x) calls the function value that f holds. f can be a local, a parameter or a captured name. b.on_click() calls a field of a function type.
fn twice(f: () -> unit) {
f()
f()
}
class Button {
on_click: () -> unit
}
fn main() {
let say = () => print("hi")
twice(say) // hi, then hi
let b = Button(on_click: say)
b.on_click() // hi
}
Closures In development
A closure is parameters => body. A closure is a value of a function type, so it goes anywhere a function type goes.
| Form | Meaning |
|---|---|
x => x + 1 |
One parameter, with no parentheses |
(a, b) => a + b |
Several parameters |
() => print("hi") |
No parameters |
(x: i64) => x * 2 |
A parameter with a type |
_ => 0 |
A parameter that the closure does not use |
x => { ... } |
A block as the body. Its last expression is the value |
fn apply(f: (i64) -> i64, x: i64) -> i64 {
f(x)
}
fn main() {
print(apply(x => x + 1, 1)) // 2
print(apply(_ => 0, 1)) // 0
let bump = (x: i64) => {
let y = x * 2
y + 1
}
print(bump(5)) // 11
}
The rules:
- The parameter types come from the expected type, so
apply(x => x + 1, 1)needs no type forx. A parameter can also have a written type, as in(x: i64) => x * 2. - The return type always comes from the body. A closure has no syntax for it. Where an expected type exists,
sisuchecks the body against its return type. _as a parameter name binds nothing. A parameter list can have it more than once.- The body is one expression, one assignment or compound assignment, or a block. An assignment has the type
unit, so() => c.count += 1is valid. - A
returnin a block body returns from the closure. So dofailand?, which act on the closure's own return type (In a closure). Abreakorcontinuecannot leave the closure for a loop around it. - A closure whose expected result type is
unitgives no value. When its body has a value, the program builds with the warning for an unused value (W0004). - A closure argument of a generic call is checked after the other arguments. Its body can fix a type argument that nothing else fixed. For a generic function such as
fold(init, f), the callfold(0, (acc, x) => acc + x)uses the0to fix the type ofacc.
Captures In development
A closure uses the names around it: each local, parameter and self that its body names. It captures them by value. The closure gets a copy when the program creates the closure.
class Counter {
mut count: i64
}
fn main() {
let mut n = 1
let show = () => n
n = 5
print(show()) // 1
print(n) // 5
let c = Counter(count: 0)
let bump = () => c.count += 1
bump()
bump()
print(c.count) // 2
}
The rules:
- A closure made before a
let mutbinding changes keeps the old value. - A captured array or string is copied as any binding of it is (Values and memory).
- A closure that captures an object shares the object. It can write the
mutfields of the object. - An assignment to a captured name is an error, also when the name is a
let mut. The closure cannot change it.
fn main() {
let mut total = 0
// The closure cannot change `total`.
let add = (x: i64) => total += x
add(2)
}
To keep a value that changes, return the new value from the closure, or keep the value in a mut field of an object.
Weak captures In development
A closure that captures an object keeps the object alive. When the object also holds the closure, as in a field, the two never get freed (Cycles). The example below makes such a cycle.
class Button {
mut on_click: (() -> unit)?
}
class Counter {
mut count: i64
button: Button
fn wire(self) {
self.button.on_click = () => self.increment()
}
fn increment(self) {
self.count += 1
}
}
fn main() {
let c = Counter(count: 0, button: Button(on_click: None))
c.wire()
}
A weak capture does not keep the object alive. The way to write a weak capture is not final, so this page shows none.
The rules:
- A name can be captured weakly when its type conforms to
AnyObject, or is an optional of such a type. A class, and a class-only interface value, conform (Bounds, Interface values). Any other capture is an error. - Inside the closure, a name that is captured weakly is an optional. Read it with
?.,??andif let, as a weak field. - When the object is gone, the closure still runs. A call such as
self?.increment()then does nothing. - A closure captures an object
xstrongly, and the program stores the closure in a place that starts atx, such asx.button.on_click. Then the object keeps itself alive, and the program builds with a warning. The fix is to capturexweakly. - A store is an assignment or a
push, as inself.handlers.push(() => self.go()). The closure can be anywhere in the stored value, for example in a constructor, an array literal or an enum value. - A closure inside the body of another closure does not count. A closure that a call passes on, or that another object stores, can still make a cycle with no warning.
Iterators In development
An iterator is a value with a method next. Each call of next gives the next element as an optional, and None when no element is left. The interface Iterator states this, with an associated type Item.
fn main() {
let xs = [10, 20, 30]
for x in xs.iter() {
print(x) // 10, 20, 30
}
let mut it = xs.iter()
while let x = it.next() {
print(x) // 10, 20, 30
}
}
An iterable is a value that can make an iterator. The interface Iterable has the associated types Item and Iter, and a method iter. A for loop takes an iterable.
The rules:
for x in ewith an array or a range works asfortells. For any othere, the type ofemust conform toIterable. The loop callse.iter()once, thennext()until the result isNone.- An array conforms to
Iterable.xs.iter()gives the iterator of an array. It holds the array, so a change to the array during the loop copies the array, and the loop sees the old elements. - Every iterator is an iterable. Its
iter()gives a copy of the iterator itself. - A range is a value.
0..nand0..=ncan be stored, passed and returned, and they are iterators ofi64. - A
forloop copies its iterator. So after abreak, the variable that held the iterator is where it was, and a secondforover it starts again. An iterator that is an object is shared, so the loop advances it. - To continue where a loop stopped, call
nextin awhile let.
Iterator methods In development
Every iterable has these methods. An adapter returns a new iterator and does nothing until the program asks it for an element. The other methods use the iterator up.
| Method | Result | Kind |
|---|---|---|
map(f) |
An iterator of the results of f |
Adapter |
filter(keep) |
An iterator of the elements for which keep is true |
Adapter |
filter_some() |
For an iterator of T?, an iterator of the T values |
Adapter |
count() |
The number of elements, an i64 |
Uses up |
sum() |
The sum, for elements of type i64 or f64 |
Uses up |
fn main() {
let words = ["sauna", "sisu", "loyly", "kiuas"]
print(words.filter(w => w.len() > 4).count()) // 3
let total = (0..10).filter(x => x % 2 == 0).map(x => x * x).sum()
print(total) // 120
print(words.map(w => w.len()).sum()) // 19
}
The rules:
- A chain such as
.filter(...).map(...).sum()builds no array between its steps. - A statement that only calls an adapter, such as
xs.map(f), builds with the warning for an unused value. A note tells thatmapdoes nothing until a method uses up the iterator.
Sisu