Closures
Uma closure é uma função escrita como valor, junto com as variáveis que ela menciona do scope em que foi escrita. Ela fica com essas variáveis depois que esse scope termina, então uma closure devolvida por uma função ainda tem o que capturou.
let twice = (a: i64) => a * 2;
let sum = (a: i64, b: i64) => { let s = a + b; s };O tipo dela é fn(A): R, o tipo de todo valor de função, então uma closure vai aonde uma função vai: um parâmetro, um valor de retorno, um campo, um elemento de um array. Uma função nomeada diretamente é um valor do mesmo tipo, e pode ser passada onde quer que uma closure possa.
Captura
Uma closure captura as variáveis que menciona por move, e um valor plain (veja tipos) é copiado. Um local capturado é uma cópia privada dentro da closure: escrever nele lá dentro escreve essa cópia, e a variável de fora não é tocada.
fn main(): i32 {
let count = 10;
let next = () => { count += 1; count };
println(`inside: ${next()}, ${next()}`);
println(`outside: ${count}`);
0
}Essa cópia vive tanto quanto a closure, e é isso que torna uma closure útil como valor de retorno: ela carrega o próprio estado.
fn make_counter(start: i64): fn(): i64 {
let n = start;
() => { n += 1; n }
}
fn main(): i32 {
let tick = make_counter(100);
tick();
tick();
println(`third tick: ${tick()}`);
0
}Um valor que não é simples passa para dentro da closure por move, e o nome de fora deixa de existir dali em diante, como depois de qualquer outro move:
let words = ["a", "bb", "ccc"];
let count = () => words.len();
println(`${count()}`);
words.len() // error: 'words' was moved on line 2; write 'words.clone()' at the move if both uses are neededO que uma closure captura, a função em volta dela entrega. Um parâmetro que uma closure menciona passa por move para dentro da closure no ponto em que a closure é criada, então a função fica com esse parâmetro, faça a closure o que fizer com ele e seja ela chamada ou não. Quem chama e usa o argumento depois é avisado disso:
fn later(xs: Array<i64>): fn(): i64 { () => xs.len() }
let xs = [1, 2, 3];
let f = later(xs);
xs.len() // error: 'xs' was given to 'later' on line 4, which keeps it; write 'xs.clone()' at the call if both uses are neededOs parâmetros da própria closure escondem os nomes em volta dela, então uma closure que lê só o seu próprio xs não captura nada.
Passar closures para funções
A biblioteca padrão recebe closures onde quer que precise de um comportamento vindo de quem chama. Uma chamada a uma função com generics infere os tipos dos parâmetros da closure a partir dos outros argumentos, então eles só são escritos quando nada mais os diz:
use std.sort.{sort_by};
use std.list.{map, filter, fold};
fn main(): i32 {
let xs = [5, 3, 8, 1, 9, 2];
let limit = 4;
let above = filter(xs, (x) => x > limit);
let squares = map(above, (x) => x * x);
let total = fold(squares, 0, (acc, x) => acc + x);
let descending = sort_by(xs, (a, b) => a > b);
println(`above ${limit}: ${above.len()} values, squares sum to ${total}`);
println(`largest first: ${descending[0]}, smallest last: ${descending[5]}`);
0
}Chamar uma função capturada não é um move. Uma closure que chama um valor de função que capturou pode ser chamada de novo, e o próprio valor de função também:
fn twice(h: fn(): i64): i64 { h() + h() }
fn seven(): i64 { 7 }
fn main(): i32 {
let f = () => seven();
println(`f() + f() = ${f() + f()}, twice(seven) = ${twice(seven)}`);
0
}Uma closure que repassa uma captura é chamada uma vez
Uma closure cujo corpo repassa uma das suas capturas - entrega a captura a um parâmetro que fica com ela, ou a devolve - abre mão desse valor quando roda. Uma segunda chamada leria o que a primeira já entregou, então uma closure assim pode ser chamada uma vez. Nada é escrito para dizer isso: o compilador lê no corpo.
struct Job { name: string, steps: Array<string> }
fn finish(j: Job): i64 takes j {
println(`${j.name} done after ${j.steps.len()} steps`);
j.steps.len()
}
fn main(): i32 {
let job = Job { name: "build", steps: ["fetch", "compile", "link"] };
let run_once = () => finish(job);
let steps = run_once();
println(`${steps} steps`);
0
}Uma segunda chamada é recusada onde está escrita:
error: 'run_once' hands on what it captured and was called on line 11, so it can be called onceUma closure que pode ser chamada uma vez pode ser:
- chamada, o que a gasta;
- ligada com
let, e a regra acompanha o nome; - passada a um parâmetro que a função chamada chama no máximo uma vez e não guarda em lugar nenhum. O compilador lê isso no corpo da função chamada: uma chamada conta uma vez, uma sequência soma, um
ifou ummatchfica com o máximo dos seus ramos, e um loop conta como muitas.
Em qualquer outro lugar ela é recusada, cada vez com uma mensagem que diz por quê:
Chamada dentro de um loop declarado depois dela:
error: 'once' hands on what it captured and is called here inside a loop, so it can be called onceGuardada em um campo, um array ou um
Option, ou devolvida:error: this closure hands on what it captured, so it can be called once; it may be called, bound with 'let' or passed to a call, and not kept herePassada a uma função que pode chamá-la duas vezes:
error: 'twice' may call 'f' more than once or keep it, and this closure hands on what it captured, so it can be called once
Uma captura de tipo plain é copiada em vez de repassada, então uma closure que passa um valor plain a um parâmetro takes não é chamada uma vez. Uma closure que nunca é chamada também não tem problema: as capturas dela são liberadas junto com ela.
O spawn de std.fiber e o start de std.thread aceitam uma closure que é chamada uma vez, porque cada um roda a sua closure exatamente uma vez: é assim que um valor passa por move para outra fiber ou thread. Veja concorrência.
No corpo de uma função com generics nenhuma closure é chamada uma vez: ela pega as capturas por borrow, e uma captura que ela repassaria é recusada como um move para fora de um borrow, do mesmo jeito em toda instância da função.
Uma closure que captura uma view
Uma view é a exceção à captura por move, porque é Copy e read-only: uma closure pode capturar uma, e o lugar para o qual ela aponta fica em borrow enquanto a closure estiver viva, então esse lugar não pode sofrer move nem ser alterado nesse meio-tempo. Uma closure assim não pode sair da função em que foi criada - não é devolvida nem passada a um lugar em que a função chamada fique com ela - porque fn(A): R não tem onde dizer a qual região a view pertence.
Compartilhar uma closure
Uma closure tem um único dono, então não aceita clone:
error: 'fn(i64): i64' contains a closure and cannot be clonedQuando dois donos precisam da mesma closure, coloque-a em um shared: um clone do shared é uma contagem a mais, e todo dono chama a mesma closure. Uma closure atrás de um shared pode ser chamada de várias fibers ou threads ao mesmo tempo, então ela não pode escrever nada do que capturou nem repassar nada; uma que faça isso é recusada no shared.
fn main(): i32 {
let base = 3;
let scale = shared (x: i64) => x * base;
let other = scale.clone();
println(`${(*scale)(2)} ${(*other)(5)}`);
0
}É assim que um servidor entrega um mesmo handler de requisição a toda conexão: um shared do handler criado antes do loop de accept, e um clone dele capturado pela fiber de cada conexão.
O que uma closure não pode fazer
- Ser comparada. Uma closure não tem igualdade, e um valor que guarda uma também não:
f == gé recusado, porque duas closures só são a mesma por identidade. - Fazer as vezes de uma função com clause. Uma função que
editsoutakesum parâmetro não pode ser usada como valor: o tipofn(A): Rnão carrega clause, então uma chamada através do valor não teria como declarar o efeito. - Aparecer em tempo de compilação. Uma closure aloca o seu ambiente, então uma
const fne uma função#[no_alloc]não podem criar uma.