Programação funcional em Rust: Closures e Iteradores
Clube do Livro #13
Closures
Clube do Livro #13
Projeto-exemplo
- Backend em Rust gera planos de exercícios personalizados.
- O algoritmo que gera o plano de treino leva em consideração muitos fatores...
- ... mas o algoritmo em si não é importante neste exemplo; para nós interessa apenas que ele é complexo e demorado. Então simularemos isso.
- Problema: queremos chamar este algoritmo apenas quando precisarmos, nosso objetivo é fazer o usuário do app esperar o mínimo possível.
Clube do Livro #13
O que são Closures?
- Closures são funções anônimas que podem capturar valores do escopo na qual foram definidas
- Elas podem ser salvas em variáveis e passadas para outras funções para que estas a utilizem.
- Funções que recebem outras funções como parâmetro são chamadas de funções de alta ordem (High Order Functions)
- Com closures, temos a habilidade de definir o código num lugar e executá-lo em outro. Isso permite melhor reutilização de código além de customização de comportamentos.
Clube do Livro #13
Sintaxe da Closure
let expensive_closure = |num| {
println!("Calculando lentamente...");
thread::sleep(Duration::from_secs(2));
num
};Clube do Livro #13
Closure: Inferência de tipos
- Não é necessário anotar os tipos dos parâmetros, porque:
- Closures não são tão expostas como as funções
- Elas geralmente são concisas e tem relevância apenas para uma pequena parte bem definida de nosso código, ao invés de um contexto genérico como no caso das funções.
- Por causa deste contexto bem limitado, o compilador do Rust consegue inferir com segurança os tipos dos parâmetros e o tipo do retorno de uma closure, da mesma forma como consegue inferir os tipos das variáveis.
Clube do Livro #13
Mas se você quiser, pode anotar sim!
let expensive_closure = |num: u32| -> u32 {
println!("Calculando lentamente...");
thread::sleep(Duration::from_secs(2));
num
};Clube do Livro #13
Aproximação com sintaxe de fn
fn add_one_v1 (x: u32) -> u32 { x + 1 }
let add_one_v2 = |x: u32| -> u32 { x + 1 };
let add_one_v3 = |x| { x + 1 };
let add_one_v4 = |x| x + 1 ;Clube do Livro #13
-
Observe que invocar as versões
add_one_v3eadd_one_v4pelo menos uma vez é obrigatório para que o código compile, pois somente assim o compilador poderá inferir os tipos e gerar as versões concretas. -
Para cada closure, apenas uma versão concreta, com tipos definidos para os argumentos, será criada
Memoization e lazy evaluation
-
Retornando ao projeto, ainda temos que resolver o problema da chamada dupla
-
Poderíamos usar uma variável, mas existe uma solução mais elegante que o uso de closures nos permite
-
O que nós iremos fazer é criar uma estrutura que guarde a nossa closure e o resultado de sua invocação: um sistema de cache bem simples. Dessa forma, o resultado será calculado apenas uma vez.
-
Com isso utilizaremos as técnicas memoization (memoização) e lazy evaluation/call-by-need (avaliação preguiçosa/chamada de acordo com necessidade)
Clube do Livro #13
Criando a estrutura
-
Para fazer essa estrutura, precisaremos especificar o tipo da closure, pois os tipos dos campos de uma struct precisam ser explicitamente definidos
-
Cada closure tem seu próprio tipo anônimo, ou seja, mesmo que duas closures tenham a mesma assinatura, seus tipos são individuais
-
Dessa forma, para definir a nossa estrutura, nós teremos que lançar mão de genéricos e trait bounds (capítulo 10)
-
A biblioteca-padrão do Rust provê três tipos de traits para closures:
Fn,FnMutouFnOnce. Nós vamos discutir as diferenças entre elas depois.
Clube do Livro #13
Limitações de nossa estrutura
-
Sempre o mesmo valor, mesmo com invocações diferentes.
-
Podemos usar um hashmap para resolver isso.
-
-
Só aceita closures com a assinatura
Fn(u32) -> u32-
Podemos usar mais tipos genéricos para dar a solução
-
Clube do Livro #13
Closures capturam o ambiente
-
Diferente de funções, closures podem capturar o ambiente e ter acesso às variáveis do contexto em que foram definidas:
fn main() {
let x = 4;
let equal_to_x = |z| z == x;
let y = 4;
assert!(equal_to_x(y));
}Clube do Livro #13
Closures capturam o ambiente
-
A captura de valores resulta numa sobrecarga para o app, pois a closure tem que usar acesso à memória.
-
Se você não precisa, não use.
-
Três formas de captura de valores para uma closure:
-
pegando a propriedade (
FnOnce); -
pegando emprestado como uma referência mutável (
FnMut); ou -
pegando emprestado como uma referência imutável (
Fn).
-
-
O compilador automaticamente escolhe qual trait usar de acordo com o uso que a nossa closure faz das variáveis
Clube do Livro #13
Closures capturam o ambiente
-
Palavra-chave
move: forçando tomada de propriedade
fn main() {
let x = vec![1, 2, 3];
let equal_to_x = move |z| z == x;
println!("ERRO: x foi movido: {:?}", x);
let y = vec![1, 2, 3];
assert!(equal_to_x(y));
}-
Na maior parte das vezes, você pode começar com a trait
Fn; o compilador te avisará se você precisar de outra trait.
Clube do Livro #13
Iteradores
Clube do Livro #13
Iteradores
-
É um design pattern (padrão de código)
-
Consiste na execução algumas tarefas em uma sequência de itens por vez
-
Um iterador é responsável pela lógica de iterar sobre cada item e determinar quando a sequência termina
-
Ao usar iteradores já prontos do Rust, você não precisa reimplementar essa lógica sozinho;
-
Iteradores no Rust são preguiçosos (lazy-evaluation): eles não têm efeito até que você chame algum método que os consuma;
Clube do Livro #13
Adaptadores consumidores
- São métodos que consomem iteradores, por exemplo:
#[test]
fn iterator_sum() {
let v1 = vec![1, 2, 3];
let v1_iter = v1.iter();
let total: i32 = v1_iter.sum();
assert_eq!(total, 6);
}-
Collect()é um muito usado: transforma um iterador numa coleção
Clube do Livro #13
Adaptadores de iterador
- Métodos que produzem outros iteradores, portanto permitem transformar um iterador em outro
-
Você pode interligar múltiplas chamadas a esses métodos para fazer transformações complexas com dados:
#[test]
fn iterator_sum() {
let v1: Vec<i32> = vec![1, 2, 3, 5, 6];
let v1_plus_even: Vec<i32> = v1.iter()
.map(|x| x + 1)
.filter(|x| x % 2 == 0)
.collect();
assert_eq!(v1_plus_even, vec![2,4,6]);
}Clube do Livro #13
Criando iteradores de coleções
#[test]
fn iterator_demonstration() {
let v1 = vec![1, 2, 3];
let mut v1_iter = v1.iter();
assert_eq!(v1_iter.next(), Some(&1));
assert_eq!(v1_iter.next(), Some(&2));
assert_eq!(v1_iter.next(), Some(&3));
assert_eq!(v1_iter.next(), None);
}Clube do Livro #13
Formas de criar um iterador
-
Existem três formas de criar um iterador de uma sequência:
-
iter()produz um iterador sobre referências imutáveis -
into_iter()produz um iterador que se apropria da coleção e retorna valores com propriedade -
iter_mut()produz um iterador com referências mutáveis
-
Clube do Livro #13
Anatomia de um iterador
- Todos os iteradores implementam a trait
Iterator
pub trait Iterator {
type Item; // capítulo 19
fn next(&mut self) -> Option<Self::Item>;
// métodos com implementação padrão
// omitidos
}Clube do Livro #13
- Basicamente o que precisamos implementar é o método
nexte o tipo associadoItem.
Criando nossos iteradores
-
Você pode criar iteradores para os tipos embutidos de Rust, como, por-exemplo, um hashmap ou uma struct.
-
Você também pode criar iteradores para os seus tipos customizados ao implementar a trait
Iterator
Clube do Livro #13
Aplicando o conhecimento
Clube do Livro #13
Melhorando nosso programa
-
Construímos um programa de linha de comando no capítulo anterior
-
Com o conhecimento adquirido sobre closures e iteradores, nós vamos fazer algumas refatorações:
-
Utilizaremos o iterador retornado por
argsdiretamente, removendo a necessidade doclone()e de slices de strings -
Refatoraremos
search()para um estilo funcional com iteradores
-
Clube do Livro #13
Performance de iteradores e closures
Clube do Livro #13
Conclusão: abstrações custo-zero
-
Embora sejam uma abstração de alto nível, iteradores são compilados quase no mesmo código de baixo-nível que você geraria
-
Iteradores são uma das abstrações de custo zero de Rust: o uso da abstração não impõe sobrecarga de tempo de execução adicional.
-
Otimização unrolling: remove a sobrecarga do código de controle do loop e, em vez disso, gera código repetitivo para cada iteração do loop. Todos os coeficientes são armazenados em registradores, o que significa que o acesso aos valores é muito rápido.
Clube do Livro #13
Obrigado, pessoal!

@felubra
twitter, github, telegram:
Referências
Clube do Livro #13
Programação funcional em Rust
By Felipe Lube de Bragança
Programação funcional em Rust
Material de apoio para apresentação do capítulo 13 do Livro do Rust em nosso canal do youtube, dia 27/12/2020 às 21h. https://www.youtube.com/watch?v=Foc2DgHcnoU&feature=youtu.be
- 43