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