Funções de ordem superior com coleções

1. Introdução

No codelab Usar tipos de função e expressões lambda no Kotlin, você aprendeu sobre funções de ordem superior, que usam outras como parâmetros e/ou retornam uma função, como repeat(). As funções de ordem superior são especialmente importantes para as coleções, porque elas ajudam a realizar tarefas comuns, como classificar ou filtrar itens e precisam de menos código. Agora que você tem uma base sólida para trabalhar com coleções, está na hora de recapitular o uso das funções de ordem superior.

Neste codelab, você vai aprender sobre várias funções que podem ser usadas em tipos de coleção, incluindo forEach(), map(), filter(), groupBy(), fold() e sortedBy(). Ao longo do processo, você vai praticar mais ainda o trabalho com expressões lambda.

Pré-requisitos

  • Conhecer os tipos de função e expressões lambda.
  • Conhecer a sintaxe de lambdas finais, como com a função repeat().
  • Conhecer diferentes tipos de coleções em Kotlin, como List.

O que você vai aprender

  • Como incorporar expressões lambda em strings.
  • Como usar várias funções de ordem superior com a coleção List, incluindo forEach(), map(), filter(), groupBy(), fold() e sortedBy().

O que é necessário

  • Um navegador da Web com acesso ao Playground Kotlin.

2. Função forEach() e modelos de strings com lambdas

Código inicial

Nos exemplos abaixo, você vai usar uma List que representa o cardápio de biscoitos de uma padaria (que delícia!) e implementar as funções de ordem superior para formatar esse cardápio de maneiras diferentes.

Para começar, configure o código inicial.

  1. Acesse o Playground Kotlin.
  2. Acima da função main(), adicione a classe Cookie. Cada instância de Cookie representa um item no menu, com um name, um price e outras informações sobre o biscoito.
class Cookie(
    val name: String,
    val softBaked: Boolean,
    val hasFilling: Boolean,
    val price: Double
)

fun main() {

}
  1. Abaixo da classe Cookie, fora da função main(), crie uma lista de biscoitos, como mostrado no exemplo abaixo. O tipo é inferido como List<Cookie>.
class Cookie(
    val name: String,
    val softBaked: Boolean,
    val hasFilling: Boolean,
    val price: Double
)

val cookies = listOf(
    Cookie(
        name = "Chocolate Chip",
        softBaked = false,
        hasFilling = false,
        price = 1.69
    ),
    Cookie(
        name = "Banana Walnut",
        softBaked = true,
        hasFilling = false,
        price = 1.49
    ),
    Cookie(
        name = "Vanilla Creme",
        softBaked = false,
        hasFilling = true,
        price = 1.59
    ),
    Cookie(
        name = "Chocolate Peanut Butter",
        softBaked = false,
        hasFilling = true,
        price = 1.49
    ),
    Cookie(
        name = "Snickerdoodle",
        softBaked = true,
        hasFilling = false,
        price = 1.39
    ),
    Cookie(
        name = "Blueberry Tart",
        softBaked = true,
        hasFilling = true,
        price = 1.79
    ),
    Cookie(
        name = "Sugar and Sprinkles",
        softBaked = false,
        hasFilling = false,
        price = 1.39
    )
)

fun main() {

}

Repetir uma lista com forEach()

A primeira função de ordem superior que você vai aprender a usar é a função forEach(). A forEach() executa a função transmitida como um parâmetro uma vez para cada item na coleção. Esse comportamento é parecido com o da função repeat() ou de uma repetição for. A lambda é executada para o primeiro elemento, depois para o segundo e assim por diante, até que ela seja executada para cada elemento da coleção. A assinatura do método fica assim:

forEach(action: (T) -> Unit)

forEach() usa um único parâmetro de ação, que é uma função do tipo (T) -> Unit.

T corresponde ao tipo de dados incluídos na coleção. Como a lambda usa um único parâmetro, é possível omitir o nome dela e se referir ao parâmetro usando it.

Use a função forEach() para mostrar os itens da lista de cookies.

  1. Em main(), chame forEach() na lista de cookies usando a sintaxe da lambda final. Como a lambda final é o único argumento nesse ponto, é possível omitir os parênteses ao chamar a função.
fun main() {
    cookies.forEach {

    }
}
  1. No corpo da lambda, adicione uma instrução println() que mostra it.
fun main() {
    cookies.forEach {
        println("Menu item: $it")
    }
}
  1. Execute o código e observe a saída. O conteúdo do objeto não aparece, apenas o nome do tipo (Cookie) e um identificador exclusivo do objeto são mostrados.
Menu item: Cookie@5a10411
Menu item: Cookie@68de145
Menu item: Cookie@27fa135a
Menu item: Cookie@46f7f36a
Menu item: Cookie@421faab1
Menu item: Cookie@2b71fc7e
Menu item: Cookie@5ce65a89

Incorporar expressões em strings

Ao aprender sobre os modelos de string, observamos que é possível usar o símbolo de cifrão ($) com um nome de variável para a inserir em uma string. No entanto, isso não funciona da forma esperada para acessar propriedades quando combinado com o operador de ponto (.).

  1. Na chamada para forEach(), modifique o corpo da lambda para inserir $it.name na string.
cookies.forEach {
    println("Menu item: $it.name")
}
  1. Execute o código. Isso insere o nome da classe, Cookie, e um identificador exclusivo para o objeto seguido por .name. O valor da propriedade name não é acessado.
Menu item: Cookie@5a10411.name
Menu item: Cookie@68de145.name
Menu item: Cookie@27fa135a.name
Menu item: Cookie@46f7f36a.name
Menu item: Cookie@421faab1.name
Menu item: Cookie@2b71fc7e.name
Menu item: Cookie@5ce65a89.name

Para acessar e incorporar as propriedades em uma string, é necessário usar uma expressão. Coloque a expressão entre chaves para fazer com que ela faça parte de um modelo de string.

2c008744cee548cc.png

A expressão lambda é colocada entre chaves. É possível acessar propriedades, realizar operações matemáticas, chamar funções, entre outras tarefas, e o valor de retorno da lambda vai ser inserido na string.

Vamos modificar o código para que o nome seja inserido na string.

  1. Coloque o it.name entre chaves para transformá-lo em uma expressão lambda.
cookies.forEach {
    println("Menu item: ${it.name}")
}
  1. Execute o código. A saída contém o name de cada Cookie.
Menu item: Chocolate Chip
Menu item: Banana Walnut
Menu item: Vanilla Creme
Menu item: Chocolate Peanut Butter
Menu item: Snickerdoodle
Menu item: Blueberry Tart
Menu item: Sugar and Sprinkles

3. Função map()

A função map() permite transformar uma coleção em uma nova com o mesmo número de elementos. Por exemplo, map() poderia transformar uma List<Cookie> em uma List<String> contendo apenas o name do biscoito, desde que você informe à função map() como criar uma String para cada item de Cookie.

e0605b7b09f91717.png

Imagine que você está criando um app que mostra um cardápio interativo de uma padaria. Ao acessar a tela do cardápio de biscoitos, o usuário vai esperar que os dados sejam apresentados de maneira lógica, como o nome seguido pelo preço. Você pode criar uma lista de strings formatadas com os dados relevantes (nome e preço) usando a função map().

  1. Remova todo o código anterior de main(). Crie uma nova variável chamada fullMenu e a defina como igual ao resultado da chamada para map() na lista de cookies.
val fullMenu = cookies.map {

}
  1. No corpo da lambda, adicione uma string formatada para incluir o name e o price de it.
val fullMenu = cookies.map {
    "${it.name} - $${it.price}"
}
  1. Mostre o conteúdo de fullMenu. Para isso, use forEach(). A coleção fullMenu retornada de map() tem o tipo List<String>, em vez de List<Cookie>. Cada Cookie em cookies corresponde a uma String em fullMenu.
println("Full menu:")
fullMenu.forEach {
    println(it)
}
  1. Execute o código. A saída corresponde ao conteúdo da lista de fullMenu.
Full menu:
Chocolate Chip - $1.69
Banana Walnut - $1.49
Vanilla Creme - $1.59
Chocolate Peanut Butter - $1.49
Snickerdoodle - $1.39
Blueberry Tart - $1.79
Sugar and Sprinkles - $1.39

4. Função filter()

A função filter() permite criar um subconjunto de itens de uma coleção. Por exemplo, você poderia usar filter() em uma lista de números para criar uma nova lista contendo apenas números divisíveis por 2.

d4fd6be7bef37ab3.png

Enquanto o resultado da função map() sempre gera uma coleção de mesmo tamanho, filter() gera uma coleção do mesmo tamanho ou menor que a original. Ao contrário de map(), a coleção resultante contém o mesmo tipo de dados. Portanto, filtrar uma List<Cookie> resulta em outra List<Cookie>.

Semelhante a map() e forEach(), filter() usa uma única expressão lambda como parâmetro. A lambda tem um único parâmetro que representa cada item na coleção e retorna um valor Boolean.

Em cada item da coleção:

  • Se o resultado da expressão lambda for true, o item vai ser incluído na nova coleção.
  • Se o resultado for false, o item não vai ser incluído na nova coleção.

Isso é útil caso você queira formar um subconjunto de dados no seu app. Por exemplo, suponha que a padaria queira destacar biscoitos em uma parte separada do cardápio. Nesse caso, você pode filter() (filtrar) a lista de cookies antes de mostrar os itens.

  1. Em main(), crie uma nova variável com o nome softBakedMenu e a defina como igual ao resultado da chamada de filter() na lista de cookies.
val softBakedMenu = cookies.filter {
}
  1. No corpo da lambda, adicione uma expressão booleana para verificar se a propriedade softBaked do biscoito é igual a true. Como softBaked é um Boolean, o corpo da lambda só precisa conter it.softBaked.
val softBakedMenu = cookies.filter {
    it.softBaked
}
  1. Mostre o conteúdo de softBakedMenu usando forEach().
println("Soft cookies:")
softBakedMenu.forEach {
    println("${it.name} - $${it.price}")
}
  1. Execute o código. O menu é mostrado como antes, mas incluindo somente os biscoitos.
...
Soft cookies:
Banana Walnut - $1.49
Snickerdoodle - $1.39
Blueberry Tart - $1.79

5. Função groupBy()

A função groupBy() pode ser usada para transformar uma lista em um mapa. Cada valor de retorno exclusivo dessa função se transforma em uma chave no mapa gerado. Os valores de cada chave correspondem a todos os itens da coleção que produziu o valor de retorno exclusivo.

54e190b34d9921c0.png

O tipo de dados das chaves é o mesmo que o tipo de retorno da função transmitida para groupBy(). O tipo de dados dos valores é uma lista de itens derivados da lista original.

Ela é difícil de explicar; então vamos começar com um exemplo simples. Na mesma lista usada anteriormente, agrupe os números em ímpares ou pares.

Para descobrir se um número é ímpar ou par, divida-o por 2 e confira se o resto da divisão é 0 ou 1. Se o resultado for 0, o número é par. Caso contrário, se o resto da divisão for 1, o número é ímpar.

Para fazer isso, você pode usar o operador de módulo (%), que divide o número ao lado esquerdo de uma expressão pelo valor à direita.

4c3333da9e5ee352.png

Em vez de retornar o resultado da divisão, como o operador de divisão (/), o operador de módulo retorna o resto. Isso é útil para verificar se um número é par ou ímpar.

4219eacdaca33f1d.png

A função groupBy() é chamada usando esta expressão lambda: { it % 2 }.

O mapa resultante tem duas chaves: 0 e 1. Cada chave tem um valor do tipo List<Int>. A lista da chave 0 contém todos os números pares e a lista da chave 1, todos os números ímpares.

Um exemplo de caso de uso real para essa função seria um app de fotos que agrupa as imagens de acordo com o conteúdo ou local em que elas foram tiradas. No cardápio da padaria, vamos dividir os biscoitos entre macios ou crocantes.

Use a função groupBy() para agrupar o cardápio de acordo com a propriedade softBaked.

  1. Remova a chamada para a função filter() da etapa anterior.

Código a ser removido

val softBakedMenu = cookies.filter {
    it.softBaked
}
println("Soft cookies:")
softBakedMenu.forEach {
    println("${it.name} - $${it.price}")
}
  1. Chame groupBy() na lista de cookies e armazene o resultado em uma variável com o nome groupedMenu.
val groupedMenu = cookies.groupBy {}
  1. Transmita uma expressão lambda que retorne it.softBaked. O tipo de retorno vai ser Map<Boolean, List<Cookie>>.
val groupedMenu = cookies.groupBy { it.softBaked }
  1. Crie uma variável softBakedMenu contendo o valor de groupedMenu[true] e uma variável crunchyMenu contendo o valor de groupedMenu[false]. Como o resultado da assinatura de Map é anulável, é possível usar o operador Elvis (?:) para retornar uma lista vazia.
val softBakedMenu = groupedMenu[true] ?: listOf()
val crunchyMenu = groupedMenu[false] ?: listOf()
  1. Adicione o código para mostrar o cardápio de biscoitos macios, seguido pelo cardápio de biscoitos crocantes.
println("Soft cookies:")
softBakedMenu.forEach {
    println("${it.name} - $${it.price}")
}
println("Crunchy cookies:")
crunchyMenu.forEach {
    println("${it.name} - $${it.price}")
}
  1. Execute o código. Usando a função groupBy(), divida a lista em duas, de acordo com o valor das propriedades.
...
Soft cookies:
Banana Walnut - $1.49
Snickerdoodle - $1.39
Blueberry Tart - $1.79
Crunchy cookies:
Chocolate Chip - $1.69
Vanilla Creme - $1.59
Chocolate Peanut Butter - $1.49
Sugar and Sprinkles - $1.39

6. Função fold()

A função fold() é usada para gerar um valor único de uma coleção. Normalmente, esse recurso é usado para calcular o preço total, somar todos os elementos de uma lista ou encontrar um valor médio.

a9e11a1aad05cb2f.png

A função fold() usa dois parâmetros:

  • Um valor inicial. O tipo de dados é inferido ao chamar a função, ou seja, o valor inicial de 0 é inferido como Int.
  • Uma expressão lambda que retorna um valor com o mesmo tipo do valor inicial.

Além disso, a expressão lambda tem dois parâmetros:

  • O primeiro é conhecido como acumulador. Ele tem o mesmo tipo de dados que o valor inicial. Você pode considerá-lo como um valor total. Cada vez que a expressão lambda é chamada, o acumulador é igual ao valor de retorno da chamada anterior.
  • O segundo parâmetro é do mesmo tipo de cada elemento da coleção.

Assim como ocorre com outras funções apresentadas, a expressão lambda é chamada para cada elemento em uma coleção. Você pode usar fold() como uma forma concisa de somar todos os elementos.

Vamos usar fold() para calcular o preço total de todos os biscoitos.

  1. Em main(), crie uma nova variável com o nome totalPrice e a defina como igual ao resultado da chamada de fold() na lista de cookies. Transmita 0.0 como valor inicial. O tipo é inferido como Double.
val totalPrice = cookies.fold(0.0) {
}
  1. Você precisa especificar os dois parâmetros para a expressão lambda. Use total para o acumulador e cookie para o elemento da coleção. Use uma seta (->) depois da lista de parâmetros.
val totalPrice = cookies.fold(0.0) {total, cookie ->
}
  1. No corpo da lambda, calcule a soma de total e cookie.price. O resultado vai ser inferido como o valor de retorno e transmitido para total na próxima vez que a lambda for chamada.
val totalPrice = cookies.fold(0.0) {total, cookie ->
    total + cookie.price
}
  1. Mostre o valor de totalPrice, formatado como uma string para facilitar a leitura.
println("Total price: $${totalPrice}")
  1. Execute o código. O resultado vai ser igual à soma dos preços na lista de cookies.
...
Total price: $10.83

7. Função sortedBy()

Ao começar a estudar sobre coleções, você aprendeu que a função sort() podia ser usada para classificar elementos. No entanto, isso não funciona em uma coleção de objetos Cookie. Como a classe Cookie tem várias propriedades, o Kotlin não consegue saber se você quer classificar name, price ou alguma outra propriedade.

As coleções em Kotlin têm uma função sortedBy() para esses casos. sortedBy() permite especificar uma lambda que retorna a propriedade que você quer classificar. Por exemplo, se você quiser classificar os itens por price, a lambda vai retornar it.price. Desde que o tipo de dados do valor esteja em uma ordem de classificação lógica, ou seja, com strings em ordem alfabética e valores numéricos em ordem crescente, os dados vão ser classificados como um conjunto desse tipo.

5fce4a067d372880.png

Use sortedBy() para ordenar a lista de biscoitos em ordem alfabética.

  1. Em main(), depois do código existente, adicione uma nova variável com o nome alphabeticalMenu e a configure como igual a uma chamada de sortedBy() na lista de cookies.
val alphabeticalMenu = cookies.sortedBy {
}
  1. Na expressão lambda, retorne it.name. A lista resultante ainda vai ser do tipo List<Cookie>, mas ela estará classificada de acordo com o name.
val alphabeticalMenu = cookies.sortedBy {
    it.name
}
  1. Mostre os nomes dos biscoitos em alphabeticalMenu. Você pode usar forEach() para mostrar um nome em cada linha.
println("Alphabetical menu:")
alphabeticalMenu.forEach {
    println(it.name)
}
  1. Execute o código. Os nomes dos biscoitos vão aparecer em ordem alfabética.
...
Alphabetical menu:
Banana Walnut
Blueberry Tart
Chocolate Chip
Chocolate Peanut Butter
Snickerdoodle
Sugar and Sprinkles
Vanilla Creme

8. Conclusão

Parabéns! Você conferiu vários exemplos de como funções de ordem superior podem ser usadas com coleções. Com elas, é possível executar operações comuns, como classificar e filtrar, em uma única linha de código, deixando seus programas mais concisos e expressivos.

Resumo

  • É possível repetir cada elemento de uma coleção usando forEach().
  • As expressões podem ser inseridas em strings.
  • map() é usada para formatar os itens de uma coleção, geralmente como um conjunto de outro tipo de dados.
  • filter() pode gerar um subconjunto de uma coleção.
  • groupBy() divide uma coleção com base no valor de retorno de uma função.
  • fold() transforma uma coleção em um valor único.
  • sortedBy() é usada para classificar uma coleção de acordo com a propriedade especificada.

9. Saiba mais (links em inglês)