Kotlin é uma linguagem multiplataforma, estaticamente tipada, atualmente podendo ser compilada para a JVM, para JavaScript e para código nativo, com a tecnologia Kotlin/Native. A linguagem chamou bastante atenção principalmente no desenvolvimento Android, que teve suporte oficializado pela Google para a plataforma.
No artigo de hoje iremos trabalhar com funções de alta ordem nas coleções em Kotlin, explicando o básico sobre elas e a utilização de funções como map, filter, reduce, groupBy, chunked, windowed etc...
Setup
Nesse artigo estarei usando:
- JVM 11
- Kotlin 1.3.21
- Gradle 5.0 (Kotlin DSL)
- IntelliJ IDEA 2019.1 EAP
O projeto com todos os exemplos desse artigo está presente no meu Github, veja aqui.
O que são coleções
Uma coleção é um agrupamento de um número variável de elementos (possivelmente zero ou infinito) que são relevantes para a solução do problema e precisam ser trabalhados em conjunto. Existem diversas implementações de coleções, como listas, sets, multisets, arvores, gráficos.
Sequencias
Como nesse artigo nós estaremos utilizando sequencias para demonstrar as funções de alta ordem, é importante entender a diferença entre ela e outros tipos básicos de coleções como listas e sets.
As sequências podem ser infinitas, a ordem dos elementos importa e seu acesso ocorre apenas em uma direção, diferente das listas, onde o acesso poder ser tanto aleatório, quanto sequencial, e diferente dos sets, as sequencias podem ter elementos repetidos, entretanto, só podem ser acessados uma única vez.
Nós utilizaremos as sequencias pois elas servem de bom exemplo de uso de funções de alta ordem, e sabendo utilizar elas em coleções com tamanhos infinitos, utilizar em coleções de tamanhos finitos será bastante simples, o que não ocorreria se utilizássemos listas por exemplo, já que não iria importar tanto o tamanho do resultado que queremos.
Preparação
Primeiramente vamos preparar alguns dados simples para trabalharmos.
E também vamos preparar um gerador simples desses dados.
Com isso nós teremos algumas estruturas para trabalharmos nos nossos exemplos, com elementos que podem ser geradas sob demanda, permitindo que tenhamos uma sequencia teoricamente infinita.
Funções de alta ordem
As funções de alta ordem nas coleções permitem fazermos certos filtros, transformações e até cálculos com base nas sequencias de dados que nós temos, de uma forma simples, legível e de fácil manutenção.
A utilização de funções de alta ordem em sequencias também permite que as operações só sejam executadas quando realmente há necessidade, evitando assim, a computação de dados desnecessária e, consequentemente, melhorando a performance da aplicação. Já em outros tipos de coleções, como em listas, essa computação será feita de forma imediata.
Sempre é importante avaliar suas necessidades antes de decidir qual implementação utilizar, no caso de coleções de tamanhos infinitos, não podemos fazer uma computação imediata pois isso implica em computar infinitos elementos, o que é impossível.
Filter
Uma das funções mais fáceis e simples de entender, na minha opinião, é o filter, que filtra todos elementos que não satisfazem um predicado especifico, por exemplo, se quisermos manter todos carros cujo modelo tenha sido criado a menos de 10 anos atrás.
Assim nós teremos uma sequencia com carros cujo modelo foi criado no máximo a 9 anos atrás, lembrando que o valor retornado pelo filter, nesse caso, é uma sequencia com elementos computados sob demanda, o que quer dizer que o filtro só será aplicado quando você precisar consumir esses elementos.
Map
Agora imagina que nós queiramos apenas o preço desses carros, nós podemos usar a função map para extrair esse valor.
O resultado da função map será uma Sequencia de preços (Decimais) dos carros cujo modelo tenha sido criado nos últimos 9 anos.
Reduce
Agora que nós temos o valor de todos os carros, podemos utilizar a função reduce para fazer cálculos com esses valores, para fins de demonstração, vamos somar os valores dos carros junto com um desconto de 10% sobre o resultado anterior somado com o atual.
Exemplo, dado a seguinte sequencia:
Nós somamos o valor de C1 + C2, damos um desconto de 10% e salvamos esse valor, logo em seguida, somamos o resultado anterior (de C1 + C2 com desconto), mais o valor de C3, e aplicamos um desconto de 10% novamente, e assim por diante.
Entretanto, dado o fato da função reduce ser uma operação terminal, todos os valores terão que ser computados, mas nossa sequencia é infinita, então nosso algoritmo iria executar eternamente. Para evitarmos isso, vamos limitar o numero de carros para 5, usando a função take.
O resultado dessa operação será um valor decimal, computado no momento da chamada da função reduce.
flatMap
Imagine agora que nós temos 3 concessionarias diferentes, cada uma com uma sequencia X de carros, mas isso não importa para nós, apenas queremos aplicar as mesmas operações anteriores sobre os carros que temos disponíveis em cada uma delas. A função flatMap vem para nos salvar, diferente do map que transforma um elemento em outro, a função flatMap irá de fato flatenar seu dado, fazendo assim com que todos valores dele agora pertençam a essa sequência, veja a figura a seguir para entender melhor.
Agora vejamos isso na prática.
Perceba que nós agora movemos o filtro para dentro do flatMap, isso porque nós queremos 3 carros de cada concessionária cujo o modelo tenha sido criado nos últimos 9 anos, como estamos lidando com sequencias infinitas, precisamos chamar a função take para limitar a 3 carros de cada concessionaria. Se o nosso filter estivesse fora, como nos outros exemplos, ele iria obter 3 carros de cada concessionária, porém iria filtrar os que não fizessem parte da regra do predicado, o resultado seria, no pior dos casos, uma sequencia com menos de 9 carros, e não é isso que queremos, queremos sempre ter 9 carros no nosso resultado.
Para simplificar esse comportamento, imagine que eu tenho x concessionarias, cada uma delas com um número indefinido de carros (nesse exemplo nos limitarem a apenas 3 concessionarias, com 5 carros cada):
Se eu fizer o take dentro do flatMap, mas filtrar fora, primeiramente iremos pegar 3 carros de cada concessionária:
E depois filtrar um por um.
Mas e se um deles não satisfizer o predicado?
Nós perderemos o elemento, e consequentemente não teremos a quantidade esperada de carros na sequencia final. Utilizando o filtro dentro do flatMap, nós escolhemos os carros a serem mantidos antes de limitarmos a 3 resultados, e assim, teremos o comportamento esperado.
GroupBy, AssociateBy e DistinctBy
Agora vamos falar de agrupamentos utilizando groupBy, associações utilizando o associateBy e distinções com o distinctBy, por serem conceitos bem simples, eu preferi unir os três na mesma seção do artigo.
O groupBy irá permitir que você agrupe diversos elementos numa mesma chave de um mapa, por exemplo, podemos agrupar por tipo de combustível:
Já o associateBy irá permitir que você associe uma única chave a um único valor, como por exemplo, o identificador único do carro.
Enquanto o distinctBy tem um propósito totalmente diferente, ele irá retornar uma sequencia cujo todos elementos são únicos, de acordo com uma chave especifica. Com isso nós podemos obter, por exemplo, carros de modelos totalmente distintos.
Lembrando que tanto groupBy e associateBy são funções terminais e irão computar todos os valores durante sua execução, por isso é importante, no caso de coleções infinitas, limitar a quantidade de elementos.
Fold
O fold, apesar de lembrar bastante o reduce, ele permite que um valor inicial seja definido e usado como acumulador. Imagina que agora você tem uma classe chamada CarTruck, aonde você guarda diversos carros para transporte, e precisa colocar todos carros dessa sequência dentro de uma instância de CarTruck.
Com isso você consegue utilizar sua classe própria como um acumulador, tendo como resultado uma instância que contem até 4 carros dentro de sua lista. Note que CarTruck é uma estrutura de dados imutáveis, sendo assim, o resultado é uma instância totalmente nova sem vinculo com a anterior.
Fold também é uma operação terminal.
Chunked
Agora vamos falar do chunked, essa é uma função que separa sua sequencias em listas com um tamanho predeterminado, por exemplo, imagine que você quer subdividir sua sequencia em pequenas coleções de 4 carros para fazer, por exemplo, um tipo de inserção ou armazenamento. Vamos reutilizar o CarTruck do exemplo anterior para demonstrar o uso do chunked.
Nós teremos como resultado, uma sequencia de CarTruck, contendo até 4 carros, pois não é obrigatório que nosso caminhão tenha exatamente 4 carros, ele pode transportar 1, 2 ou 3, por exemplo.
Chunked pode ser utilizado de várias maneiras, desde inserção de uma quantidade especifica de dados, em casos onde a operação é custosa para ser feita para cada elemento, ou para resolver outros tipos de problemas.
Windowed
Por fim temos windowed, apesar de muito parecido com o chunked, o windowed, traz (como o nome diz) janelas da sua lista, contendo todas as gamas de elementos, dado um step especifico.
Segundo a documentação, a visualização retornada pelo o windowed será a mesma que se você estivesse olhando para a coleção por uma janela corrediça (como a documentação tenta explicar), ou como eu explicaria, dado nosso exemplo, imagina que você tem uma fila de carros indo direto para uma ducha do lava-rápido, e você está olhando fixamente para uma direção e vendo 3 carros (size); a cada carro que passar (step), você terá a visão de um carro atrás, e perderá a visão do carro da frente.
Vamos simular esse caso, gerando 12 carros, e criando janelas com 3 de tamanho, e 1 de “passo” (em outras palavras, quantos carros você perde de vista a cada iteração), e vamos permitir janelas parciais, assim, quando estiver chegando no final da esteira, você verá 2, e depois 1 carro apenas, se não permitirmos janelas parciais, quando faltar menos elementos que o tamanho da janela, a iteração acaba.
Nesse nosso exemplo, também vamos mapear os carros para um nome com um numero incremental, para visualizarmos melhor os resultados das janelas.
Teremos na nossa lista, então, o seguinte resultado
Podemos também mudar o step da nossa janela, para 2, tendo o seguinte resultado:
Agora retomando ao exemplo com step 1 (o padrão), e vamos desabilitar janelas parciais, temos o seguinte resultado:
Veja como as duas ultimas coleções, nesse caso, tem 3 elementos, as janelas apenas acabaram antes, quando comparado ao exemplo com janelas parciais, que você continua visualizando os elementos mesmo quando o tamanho da lista é menor que o size definido.
Agora voltando a falar do menino chunked, ele não passa de um windowed com janelas parciais, onde o size e o step tem os mesmo valores, ou seja, o nosso exemplo com chunked não é diferente do exemplo abaixo: