
Uma jornada rumo aos iterators
Atenção: este texto foi traduzido, editado e adaptado por @nawarian. Você encontra o texto original, escrito em 2015 em inglês, no blog parceiro Hoverbear.org através deste link.
Uma das minhas funcionalidades favoritas em Rust são os iterators. Eles são uma forma rápida, segura e preguiçosa (lazy) de trabalhar com estruturas de dados, streams e outras aplicações mais criativas.
Você pode brincar com iterators utilizando o website http://play.rust-lang.org/ enquanto navega aqui. Este artigo não substitui a documentação oficial ou a experiência de executar os códigos por si.
Nosso primeiro iterator
Um é pouco, mas dois é bom. Vamos pegar uma lista de valores e dobrá-los!
Já neste exemplo simples, podemos observar algumas coisas interessantes:
.iter()pode ser usado com vários tipos diferentes.- Precisamos declarar um valor e só depois criar um iterator para ele. Do contrário o valor não vai viver o suficiente para ser iterado. (Iterators são preguiçosos (lazy) e nem sempre são donos de seus valores).
.collect::<Vec<usize>>()itera a lista completamente e guarda seus valores num tipo de coleção que especificarmos. (Se você quiser escrever a sua própria estrutura de dados, dá uma olhada aqui)- Um iterator pode ser utilizado e encadeado! Você não precisa necessariamente chamar
.collect()depois de uma chamada ao.map().
n de cada vez
Você pode acessar o próximo elemento de um iterator com .next(). Se você quiser pegar somente uma porção, pode utilizar .take(n) para criar um novo iterator que percorre os próximos n elementos.
Quer jogar fora alguns dados? Use .skip(n) para pular n elementos.
// Saída:
// Some(1)
// [4, 5]
Observando o comportamento preguiçoso (lazy)
Eu comentei que iterators são preguiçosos, mas o primeiro exemplo não demonstrou este comportamento. Vamos utilizar .inspect() para observar como Rust se comporta.
A saída é:
Antes do map: 1
Primeiro map: 10
Segundo map: 15
Antes do map: 2
Primeiro map: 20
Segundo map: 25
Antes do map: 3
Primeiro map: 30
Segundo map: 35
Como você pôde perceber, as funções map apenas executam quando o iterator avança. (Do contrário nós veríamos 1, 2, 3, 10, ...)
Note como .inspect() passa para seu callback um &x em vez de &mut ou o valor em si. Isto evita mutações e assegura que sua inspeção não vai bagunçar a pipeline de dados.
Isto gera uns efeitos colaterais bem bacanas como, por exemplo, iterators infinitos ou cíclicos.
// Saída [1, 2, 3, 1, 2, 3, 1, 2, 3]
Algo em comum
Não fique aí achando que [1, 2, 3] e outros slices são as únicas coisas das quais você pode criar iterators.
Muitas estruturas de dados dão implementam esta trait: podemos utilizar Vectors e DecDeques! Vejamos algumas coisas que implementam .iter().
use VecDeque;
// Saída [2, 4, 6]
Viu como fizemos o collect num VecDeque? Isto só é possível porque ele implementa FromIterator.
Você deve estar pensando "Ah rá! Aposto que você não consegue usar um HashMap ou uma árvore ou coisa do tipo, Hoverbear!". Errado! É possível, sim!
use HashMap;
// [(1, 100), (3, 300), (2, 200)]
Quando iteramos um HashMap a função .map() muda para aceitar uma tupla, e .collect() aceita tuplas. E é claro, você também pode fazer um collect de volta para um HashMap (ou qualquer outra coisa).
Você notou como a ordem mudou? Isto acontece porque HashMaps não são necessariamente ordenados. Lembre-se sempre disso!
Tente mudar o código para converter um Vec<(u64, u64)> num HashMap.
Escrevendo um iterator
Tá, nós conseguimos experimentar algumas das coisas que podem implementar iterators. Mas a gente também pode fazer um nosso! Que tal um iterator que conta alguma coisa infinitamente?
// Saída [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20]
Nós não precisamos chamar .iter() aqui. Faz sentido, porque nós já estamos implementando um iterator e não transformando alguma coisa num iterator como fizemos antes.
Tente mudar os valores de current e .take().
Viu como nós podemos usar .take() e outras funções sem ter de implementar cada uma separadamente para nosso novo iterator? Se você olhar a documentação do módulo iter, verá que há várias traits como Iterator ou RandomAccessIterator.
Intervalos com o operador Range
Através dos exemplos abaixo você verá o uso da sintaxe x..y, que cria um intervalo (range). Intervalos implementam Iterator então não é necessário chamar .iter() neles. Você também pode usar (0..100).step_by(2) se quiser realizar pular alguns elementos a cada iteração.
Note que intervalos são abertos, não inclusivos.
0..5 ==
2..6 ==
Também é possível utilizar intervalos como índices de coleção:
// Saída:
// [0, 1, 2, 3, 4]
// [2, 3, 4]
// [7, 8, 9]
Saquei: utilizar .step_by() não funciona desse jeito já que não implementa Idx, mas Range implementa.
Encadear e compactar iterators
Juntar iterators de formas diferentes nos permite escrever códigos muito expressivos.
// Chained: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
// Zipped: [(0, 5), (1, 6), (2, 7), (3, 8), (4, 9)]
.zip() te permite juntar iterators. Já o .chain() cria um iterator "extendido".
Sim, também existe o .unzip().
Tente usar .zip() com dois slices do tipo usize e depois fazer um .collect() dessas tuplas para criar um HashMap.
Vamos dar poder à curiosidade
.count(), .max_by(), .min_by(), .all() e .any() são formas comuns de verificar o que há dentro de um iterator.
// Isto só funciona com rust nightly no momento em que o artigo foi escrito
// Saída:
// Mais velho: Some(Urso { especie: Pardo, idade: 16 })
// Mais novo: Some(Urso { especie: Marrom, idade: 5 })
// Pelo menos um urso polar?: true
// Todos são menores de idade? (<18): true
Tente utilizar o mesmo iterator em todas as chamadas no exemplo acima. .any() (algum) é o único que empresta (borrow) de forma mutável e não funciona igual aos outros. Isto porque esta função talvez não consuma o iterator por completo.
Filter, Map, Redu... não... Fold!
Se você, assim como eu, tem costume de programar em Javascript você provavelmente conhece a sagrada trindade .filter(), .map() e .reduce(). Estas funções também estão disponíveis em Rust, mas .reduce() é chamado de .fold() (e eu meio que prefiro esse nome).
Um exemplo:
O snippet acima poderia ser simplificado para:
Estas três funções tornam Rust um tanto flexível quanto expressivo.
Split & Scan
Também tem o scan caso você precise de uma variação de fold que faz um yield do resultado a cada iteração. Isto é bem útil se você precisa de um certo valor acumulado e quiser verificar este valor a cada iteração.
Dividir um iterator em duas partes também é possível. Você pode usar uma função de agrupamento que retorna um boolean com partition.
Vamos usar esses dois conceitos para:
- dividir um slice grandão;
- agrupá-lo entre pares e ímpares;
- somar os grupos progressivamente; e
- verificar que o pares são sempre menores que a soma dos ímpares, já que pares começam com 0.
// Saída:
// Par sempre menor: true
Scan pode ser utilizado para gerar dados como médias móveis. Isto é útil quando se lê arquivos, dados e sensores. Particionar dados é uma tarefa comum quando se estiver organizando dados.
Outra operação comum é agrupar elementos com base num valor específico. Se você estiver buscando algo como o _.groupBy() do Lodash, calma lá. Considerando que Rust possui BTreeMap, HashMap, VecMap e outros tipos de dados, o nosso método de agrupamento precisa ser genérico.
Vamos ver um exemplo simples. Vamos escrever um iterator infinito que repete o intervalo de 0 a 5. No seu código esse iterator poderia utilizar structs ou tuplas, mas por agora, vamos utilizar somente inteiros.
Vamos agrupá-los em três categorias: zeros, cincos e todo o resto.
use HashMap;
// Saída: {Zero: [0, 0, 0, 0], Cinco: [5, 5, 5], Outro: [1, 2, 3, 4, 1, 2, 3, 4, 1, 2, 3, 4, 1]}
É meio que inútil guardar todos os valores replicados. Tente mudar o exemplo acima para retornar o número de ocorrências em vez de valores. Se quiser ir ainda mais longe, tente utilizar este método num valor mais complexo como uma struct, você também pode mudar quais chaves são utilizadas.
Flanqueado
A trait DoubleEndedIterator é útil em alguns casos. Por exemplo, quando você precisa dos comportamentos de uma fila e pilha ao mesmo tempo.
// Some(0)
// Some(9)
// Some(1)
// Some(8)
Agora vai brincar!
Agora tá na hora de você fazer uma pausa, preparar um cházinho e abrir o Playground. Tente mudar um dos exemplos acima, brinque com algumas outras coisas dos links de documentação abaixo e, no mais, divirta-se.
Se você travar, não se preocupe! Procure na internet se o erro não fizer sentido nenhum pra ti. Rust também tem uma comunidade muito ativa nos domínios *.rust-lang.org assim como no GitHub e no Stack Exchange. Sinta-se convidad@ a me enviar um email ou dar um alô no IRC também.
O que a gente viu é só a superfície... um mundo imenso nos aguarda.




Comentários