Pular para o conteúdo

Fn, FnMut e FnOnce em Rust

Na página de Closures você aprendeu a escrever uma closure (função anônima), como ela captura variáveis e como pode alterá-las. Esta página responde à próxima pergunta: como você escreve a sua própria função que recebe uma closure? E como devolver uma closure de dentro de uma função?

A resposta são três traits (características) pequenas: Fn, FnMut e FnOnce. Pense nelas como as regras de uma biblioteca. Alguns visitantes só leem os livros (Fn). Outros fazem anotações nas margens (FnMut). E alguns levam o livro para casa e nunca mais devolvem (FnOnce). O Rust olha o que a sua closure faz e dá a ela a etiqueta certa. Se as traits parecerem assustadoras no começo, não se preocupe. São só três, e o compilador escolhe a certa para você.

Toda closure tem o seu próprio tipo único, inventado pelo compilador. Você não consegue escrever o nome desse tipo. Até duas closures idênticas têm tipos diferentes:

fn main() {
let mut rules = vec![|price: u32| price * 2];
rules.push(|price: u32| price * 2);
}
error[E0308]: mismatched types
--> src/main.rs:3:16
|
3 | rules.push(|price: u32| price * 2);
| ---- ^^^^^^^^^^^^^^^^^^^^^^ expected closure, found a different closure
|
= note: no two closures, even if identical, have the same type
= help: consider boxing your closure and/or using it as a trait object

O compilador reclama que esperava uma closure e encontrou outra closure: nem duas closures idênticas têm o mesmo tipo.

Então uma função não pode dizer “me dê uma closure do tipo X”. Em vez disso, ela diz “me dê qualquer tipo que possa ser chamado deste jeito”. Isso é um trait bound (restrição de trait), igual aos que você viu nas páginas de Genéricos e Traits:

fn price_after<F: Fn(u32) -> u32>(rule: F, price: u32) -> u32 {
rule(price)
}
fn main() {
let coupon = 15;
let half_price = |price: u32| price / 2;
let use_coupon = |price: u32| price - coupon;
println!("{}", price_after(half_price, 80));
println!("{}", price_after(use_coupon, 80));
}

Saída:

40
65
▶ Experimente você mesmo

Leia F: Fn(u32) -> u32 assim: “F é algum tipo que você pode chamar com um u32, e ele devolve um u32.” As duas closures se encaixam, mesmo tendo tipos diferentes.

O compilador olha o que o corpo da closure faz com os valores que ela capturou:

  • Fn: ela só lê os valores. Você pode chamá-la quantas vezes quiser.
  • FnMut: ela altera os valores. Você pode chamá-la várias vezes, mas ela precisa de mut.
  • FnOnce: ela entrega os valores (move para fora). Você só pode chamá-la uma vez, porque depois disso o valor já foi embora.
fn main() {
let shop = String::from("Padaria da Esquina");
// Fn: só lê `shop`
let greet = || println!("Bem-vindo à {shop}!");
greet();
greet();
// FnMut: altera `orders`
let mut orders = 0;
let mut take_order = || {
orders += 1;
println!("pedido nº {orders} anotado");
};
take_order();
take_order();
// FnOnce: devolve `receipt` para quem chamou
let receipt = String::from("2 croissants, 1 café");
let hand_over = || receipt;
let customer_copy = hand_over();
println!("o cliente recebeu: {customer_copy}");
}

Saída:

Bem-vindo à Padaria da Esquina!
Bem-vindo à Padaria da Esquina!
pedido nº 1 anotado
pedido nº 2 anotado
o cliente recebeu: 2 croissants, 1 café
▶ Experimente você mesmo

A hand_over retorna o próprio receipt, e não uma cópia. Depois que o recibo é entregue, a closure não tem mais nada para dar. É por isso que ela é FnOnce.

Esta é a imagem para guardar na cabeça. Imagine três círculos, um dentro do outro:

  • O círculo grande de fora é o FnOnce. Toda closure pode ser chamada pelo menos uma vez, então todas as closures estão aqui.
  • Dentro dele há um círculo menor, o FnMut: closures que podem ser chamadas de novo e de novo.
  • Bem no meio fica o Fn: closures que podem ser chamadas de novo e de novo sem mudar nada.

Então toda closure Fn também é FnMut e FnOnce. Isso importa quando você escreve uma função. Um parâmetro que pede FnOnce aceita qualquer closure. Um parâmetro que pede Fn é o mais exigente.

Uma coisa que surpreende muita gente: a palavra-chave move não escolhe a trait. move || println!("{shop}") toma o ownership (posse) de shop, mas só lê o valor, então continua sendo Fn. Quem decide é só o corpo.

Existem três jeitos de escrever o mesmo bound. Todos significam a mesma coisa, então escolha o que ficar mais legível:

// 1. Um parâmetro genérico com bound
fn apply<F: Fn(i32) -> i32>(f: F, x: i32) -> i32 {
f(x)
}
// 2. `impl Fn` na posição de argumento: mais curto, sem precisar de nome
fn apply_twice(f: impl Fn(i32) -> i32, x: i32) -> i32 {
f(f(x))
}
// 3. Uma cláusula `where`: fica organizado quando o bound cresce
fn apply_to_all<F>(f: F, scores: &[i32]) -> Vec<i32>
where
F: Fn(i32) -> i32,
{
scores.iter().map(|&s| f(s)).collect()
}
fn main() {
let bonus = 10;
let add_bonus = |score: i32| score + bonus;
println!("{}", apply(add_bonus, 50));
println!("{}", apply_twice(add_bonus, 50));
println!("{:?}", apply_to_all(add_bonus, &[40, 65, 90]));
}

Saída:

60
70
[50, 75, 100]
▶ Experimente você mesmo

Talvez você esteja se perguntando como conseguimos passar add_bonus três vezes. Uma closure que só lê o que capturou é barata de copiar, então o Rust a copia a cada vez.

Agora o FnMut e o FnOnce. Com FnMut, o parâmetro precisa de mut, porque chamar a closure altera o que está dentro dela. Com FnOnce, você pode chamá-la uma vez, e acabou:

fn process_tasks<F: FnMut(&str)>(tasks: &[&str], mut on_done: F) {
for task in tasks {
on_done(task);
}
}
fn on_shutdown<F: FnOnce()>(cleanup: F) {
println!("desligando...");
cleanup();
}
fn main() {
let mut finished = Vec::new();
process_tasks(&["enviar e-mails", "fazer backup do banco"], |task| {
finished.push(task.to_string());
});
println!("concluídas: {:?}", finished);
on_shutdown(|| {
// Este laço toma posse de `finished`, então a closure é FnOnce
for task in finished {
println!("arquivada: {task}");
}
});
}

Saída:

concluídas: ["enviar e-mails", "fazer backup do banco"]
desligando...
arquivada: enviar e-mails
arquivada: fazer backup do banco
▶ Experimente você mesmo

Uma boa regra prática: peça a trait mais flexível com que a sua função consegue viver. Se você chama a closure só uma vez, peça FnOnce, e quem chama pode passar qualquer coisa. Você vê isso na biblioteca padrão também: Option::map recebe FnOnce, enquanto Iterator::map recebe FnMut.

O compilador pega toda incompatibilidade, e aqui as mensagens dele são bem amigáveis. Digamos que uma função pede Fn, mas a sua closure altera um contador:

fn run_report<F: Fn()>(f: F) {
f();
f();
}
fn main() {
let mut runs = 0;
run_report(|| {
runs += 1;
println!("relatório executado {runs}");
});
}
error[E0594]: cannot assign to `runs`, as it is a captured variable in a `Fn` closure
--> src/main.rs:9:9
|
1 | fn run_report<F: Fn()>(f: F) {
| - change this to accept `FnMut` instead of `Fn`
...
8 | run_report(|| {
| ---------- -- in this closure
| |
| expects `Fn` instead of `FnMut`
9 | runs += 1;
| ^^^^^^^^^ cannot assign

O erro diz que não dá para atribuir a runs dentro de uma closure Fn, e ele até mostra a correção: aceite FnMut e coloque mut no parâmetro. Com essa mudança, o programa imprime relatório executado 1 e relatório executado 2:

fn run_report<F: FnMut()>(mut f: F) {
f();
f();
}

O outro erro clássico é chamar duas vezes uma closure FnOnce:

fn main() {
let report = String::from("vendas do 3º trimestre");
let send = || {
let sent = report;
println!("enviando {sent}");
};
send();
send();
}
error[E0382]: use of moved value: `send`
--> src/main.rs:8:5
|
7 | send();
| ------ `send` moved due to this call
8 | send();
| ^^^^ value used here after move
|
note: closure cannot be invoked more than once because it moves the variable `report` out of its environment
--> src/main.rs:4:20
|
4 | let sent = report;
| ^^^^^^

A nota diz com todas as letras: a closure não pode ser chamada mais de uma vez, porque a primeira chamada entregou o report, e não sobrou nada para a segunda. Se você precisa mesmo de duas chamadas, use um empréstimo (let sent = &report;) e a closure vira Fn. Se você passar uma closure assim para uma função que pede Fn, recebe uma mensagem parecida: error[E0507]: cannot move out of report, a captured variable in an Fn closure (“não é possível mover report para fora, uma variável capturada em uma closure Fn”).

Uma função também pode construir uma closure e devolvê-la. Você escreve o tipo de retorno como impl Fn(i32) -> i32, que significa “algum tipo de closure; você não precisa saber o nome dele”:

fn make_adder(amount: i32) -> impl Fn(i32) -> i32 {
move |x| x + amount
}
fn make_multiplier(factor: i32) -> impl Fn(i32) -> i32 {
move |x| x * factor
}
fn main() {
let add_tip = make_adder(5);
let triple = make_multiplier(3);
println!("{}", add_tip(20));
println!("{}", triple(7));
println!("{}", add_tip(triple(10)));
}

Saída:

25
21
35
▶ Experimente você mesmo

Por que o move? Sem ele, a closure só pegaria amount emprestado. Mas amount pertence à make_adder e desaparece no instante em que make_adder retorna. A closure ficaria segurando uma referência para o nada. O Rust se recusa:

error[E0373]: closure may outlive the current function, but it borrows `amount`, which is owned by the current function
--> src/main.rs:2:5
|
2 | |x| x + amount
| ^^^ ------ `amount` is borrowed here
| |
| may outlive borrowed value `amount`
|
help: to force the closure to take ownership of `amount` (and any other referenced variables), use the `move` keyword
|
2 | move |x| x + amount
| ++++

A mensagem avisa que a closure pode viver mais do que a função, mas pega emprestado amount, que pertence a ela, e sugere usar move. Com move, a closure leva a sua própria cópia de amount junto, como levar a marmita com você em vez de deixá-la na geladeira de outra pessoa.

impl Fn representa um tipo escondido. Então você não pode retornar uma closure no ramo if e outra diferente no ramo else. Também não pode colocar closures diferentes no mesmo Vec. Para as duas tarefas, coloque cada closure em uma caixa: Box<dyn Fn(f64) -> f64> significa “qualquer closure com este formato, guardada no heap”. É a mesma ideia da página de Trait objects.

Aqui está um caixa de loja que aplica, em ordem, uma lista de regras de preço com nome:

fn shipping_rule(express: bool, fee: f64) -> Box<dyn Fn(f64) -> f64> {
if express {
Box::new(move |total| total + fee * 2.0)
} else {
Box::new(move |total| total + fee)
}
}
fn main() {
let member_percent = 10.0;
let coupon = 5.0;
let rules: Vec<(&str, Box<dyn Fn(f64) -> f64>)> = vec![
("desconto de sócio", Box::new(move |total: f64| total * (1.0 - member_percent / 100.0))),
("cupom", Box::new(move |total: f64| total - coupon)),
("frete padrão", shipping_rule(false, 3.5)),
];
let mut total = 80.0;
println!("início: {total:.2}");
for (name, rule) in &rules {
total = rule(total);
println!("depois de {name}: {total:.2}");
}
}

Saída:

início: 80.00
depois de desconto de sócio: 72.00
depois de cupom: 67.00
depois de frete padrão: 70.50
▶ Experimente você mesmo

O Box custa uma pequena alocação no heap, e cada chamada passa por um ponteiro. Isso quase nunca é um problema. Use impl Fn quando houver um tipo de closure, e Box<dyn Fn> quando houver vários.

Uma struct (estrutura) também pode guardar uma closure. Como você não consegue dar nome ao tipo da closure, você torna a struct genérica sobre ele. Aqui está um botãozinho que executa algum código quando é clicado:

struct Button<F: Fn()> {
label: String,
on_click: F,
}
impl<F: Fn()> Button<F> {
fn click(&self) {
println!("[{}] clicado", self.label);
(self.on_click)();
}
}
fn main() {
let user = String::from("Ada");
let save = Button {
label: String::from("Salvar"),
on_click: move || println!("salvando o rascunho de {user}"),
};
save.click();
save.click();
}

Saída:

[Salvar] clicado
salvando o rascunho de Ada
[Salvar] clicado
salvando o rascunho de Ada
▶ Experimente você mesmo

Repare nos parênteses em (self.on_click)(). Eles dizem ao Rust “pegue o campo e depois chame-o”. Se o botão precisar alterar o que capturou a cada clique, use F: FnMut() e faça o método receber &mut self.

Funções comuns também podem ser passadas de um lado para o outro. O tipo delas é escrito com um fn minúsculo, como fn(i32) -> i32. Isso é um ponteiro de função, um simples endereço de um trecho de código. Uma closure que não captura nada também pode virar um:

fn double(x: i32) -> i32 {
x * 2
}
fn square(x: i32) -> i32 {
x * x
}
fn apply_op(op: fn(i32) -> i32, x: i32) -> i32 {
op(x)
}
fn main() {
println!("{}", apply_op(double, 4));
println!("{}", apply_op(|x| x - 1, 4));
let steps: [fn(i32) -> i32; 3] = [double, square, |x| x + 1];
let mut value = 3;
for step in steps {
value = step(value);
}
println!("{value}");
}

Saída:

8
3
37
▶ Experimente você mesmo

Todos os ponteiros de função compartilham um só tipo, então você pode colocá-los em um array sem Box. O porém é que uma closure que captura alguma coisa não pode virar ponteiro de função. O compilador avisa: closures can only be coerced to fn types if they do not capture any variables (closures só podem ser convertidas em tipos fn se não capturarem nenhuma variável). A boa notícia vale no sentido contrário: toda função comum também implementa Fn, FnMut e FnOnce, então você pode passar double para a apply de antes. É por isso que a maior parte do código usa as traits de closure: elas aceitam as duas coisas.

Esquecer o mut em um parâmetro FnMut. Chamar uma FnMut altera a closure, então a variável que a guarda precisa ser mut:

error[E0596]: cannot borrow `on_done` as mutable, as it is not declared as mutable
--> src/main.rs:3:9
|
3 | on_done(task);
| ^^^^^^^ cannot borrow as mutable
|
help: consider changing this to be mutable
|
1 | fn process_tasks<F: FnMut(&str)>(tasks: &[&str], mut on_done: F) {
| +++

O compilador diz que não consegue pegar on_done emprestado como mutável e sugere exatamente onde colocar o mut.

Retornar closures diferentes de -> impl Fn. Duas closures são dois tipos, mesmo que pareçam iguais:

fn shipping_rule(express: bool, fee: f64) -> impl Fn(f64) -> f64 {
if express {
move |total| total + fee * 2.0
} else {
move |total| total + fee
}
}
error[E0308]: `if` and `else` have incompatible types
--> src/main.rs:5:9
|
2 | / if express {
3 | | move |total| total + fee * 2.0
| | ------------------------------
| | |
| | the expected closure
| | expected because of this
4 | | } else {
5 | | move |total| total + fee
| | ^^^^^^^^^^^^^^^^^^^^^^^^ expected closure, found a different closure
6 | | }
| |_____- `if` and `else` have incompatible types
|
= note: expected closure `{closure@src/main.rs:3:9: 3:21}`
found closure `{closure@src/main.rs:5:9: 5:21}`
= note: no two closures, even if identical, have the same type
= help: consider boxing your closure and/or using it as a trait object
help: you could change the return type to be a boxed trait object
|
1 - fn shipping_rule(express: bool, fee: f64) -> impl Fn(f64) -> f64 {
1 + fn shipping_rule(express: bool, fee: f64) -> Box<dyn Fn(f64) -> f64> {
|
help: if you change the return type to expect trait objects, box the returned expressions
|
3 ~ Box::new(move |total| total + fee * 2.0)
4 | } else {
5 ~ Box::new(move |total| total + fee)
|

O if e o else têm tipos incompatíveis. Faça o que o compilador sugere: retorne Box<dyn Fn(f64) -> f64> e embrulhe cada closure em Box::new(...).

Chamar um campo com closure como se fosse um método. Escrever self.on_click() faz o Rust procurar um método chamado on_click:

error[E0599]: no method named `on_click` found for reference `&Button<F>` in the current scope
--> src/main.rs:9:14
|
9 | self.on_click();
| ^^^^^^^^ field, not a method
|
help: to call the type parameter stored in `on_click`, surround the field access with parentheses
|
9 | (self.on_click)();
| + +

A mensagem explica que on_click é um campo, não um método, e pede para envolver o acesso ao campo com parênteses.

Escreva uma função ticket_dispenser(start: u32) que retorna uma closure. Cada chamada da closure dá o próximo número de senha: start, depois start + 1, e assim por diante. Crie dois distribuidores (uma padaria começando em 100 e uma farmácia começando em 1) e mostre que eles contam separadamente. Dica: a closure altera o próprio contador, então o tipo de retorno é impl FnMut() -> u32, e você vai precisar de move.

Ver solução
fn ticket_dispenser(start: u32) -> impl FnMut() -> u32 {
let mut next = start;
move || {
let ticket = next;
next += 1;
ticket
}
}
fn main() {
let mut bakery = ticket_dispenser(100);
let mut pharmacy = ticket_dispenser(1);
println!("padaria: {}", bakery());
println!("padaria: {}", bakery());
println!("farmácia: {}", pharmacy());
println!("padaria: {}", bakery());
}

Saída:

padaria: 100
padaria: 101
farmácia: 1
padaria: 102
▶ Experimente você mesmo

Cada distribuidor moveu o seu próprio next para dentro, então eles nunca compartilham um contador. As variáveis bakery e pharmacy precisam de mut porque são FnMut.

Escreva retry(max_attempts, task). Ela chama task até receber Ok, ou até ter tentado max_attempts vezes. Imprima uma linha para cada tentativa que falhar. Teste com uma closure que falha duas vezes com "servidor ocupado" e depois dá certo. Dica: a closure de teste conta as próprias chamadas, então o bound é FnMut() -> Result<String, String>, e o parâmetro precisa de mut.

Ver solução
fn retry<F>(max_attempts: u32, mut task: F) -> Result<String, String>
where
F: FnMut() -> Result<String, String>,
{
for attempt in 1..=max_attempts {
match task() {
Ok(value) => return Ok(value),
Err(e) => println!("tentativa {attempt} falhou: {e}"),
}
}
Err(String::from("desisti"))
}
fn main() {
let mut calls = 0;
let result = retry(5, || {
calls += 1;
if calls < 3 {
Err(String::from("servidor ocupado"))
} else {
Ok(String::from("dados baixados"))
}
});
println!("{:?} depois de {calls} chamadas", result);
}

Saída:

tentativa 1 falhou: servidor ocupado
tentativa 2 falhou: servidor ocupado
Ok("dados baixados") depois de 3 chamadas
▶ Experimente você mesmo

A retry chama a tarefa mais de uma vez, então FnOnce não serve. E a tarefa altera calls, então Fn também não serve. FnMut é exatamente o certo. Quando a retry retorna, a closure terminou o seu trabalho, então a main pode ler calls de novo.

  • Toda closure tem o seu próprio tipo único, então as funções recebem closures por meio de um trait bound como F: Fn(i32) -> i32.
  • Fn só lê o que capturou, FnMut altera, e FnOnce move os valores para fora e só pode ser chamada uma vez. Quem decide é o corpo, não o move.
  • Toda Fn também é FnMut e FnOnce. Peça a trait mais flexível de que a sua função precisa.
  • Escreva os bounds como <F: Fn(..)>, impl Fn(..) ou em uma cláusula where. Parâmetros FnMut precisam de mut.
  • Retorne uma closure com -> impl Fn(..) (geralmente com move). Use Box<dyn Fn(..)> para closures diferentes vindas de ramos de um if ou guardadas em um Vec.
  • fn(i32) -> i32 é um ponteiro de função. Funções comuns e closures que não capturam nada se encaixam nele.

Teste o que aprendeu

1. Uma closure faz total += price em uma variável capturada. Qual é a trait mais restrita que ela implementa?

2. A sua função chama o parâmetro de closure exatamente uma vez. Qual bound aceita mais closures?

3. Por que make_adder(amount: i32) -> impl Fn(i32) -> i32 precisa de move |x| x + amount?

4. Você quer um Vec com várias closures de desconto diferentes. Que tipo os elementos precisam ter?

Pontuação: 0 / 4