Funciones de orden superior con colecciones

1. Introducción

En el codelab Cómo usar tipos de funciones y expresiones lambda en Kotlin, aprendiste sobre funciones de orden superior, que son funciones que toman otras funciones como parámetros o muestran una función, como repeat(). Las funciones de orden superior son especialmente relevantes para las colecciones, ya que te ayudan a realizar tareas comunes, como ordenar o filtrar, con menos código. Ahora que tienes una base sólida que funciona con las colecciones, es hora de revisar las funciones de orden superior.

En este codelab, aprenderás sobre una variedad de funciones que se pueden usar en tipos de colección, incluidas las siguientes:forEach(), map(), filter(), groupBy(), fold() y sortedBy(). En el proceso, adquirirás práctica adicional para trabajar con expresiones lambda.

Requisitos previos

  • Conocimientos de tipos de funciones y expresiones lambda
  • Conocimientos de la sintaxis lambda final, como el caso de la función repeat()
  • Conocimientos de diversos tipos de colecciones en Kotlin, como List

Qué aprenderás

  • Cómo incorporar expresiones lambda en strings
  • Cómo usar varias funciones de orden superior con la colección List, incluidas forEach(), map(), filter(), groupBy(), fold() y sortedBy()

Requisitos

  • Un navegador web con acceso a Playground de Kotlin

2. forEach() y plantillas de string con lambdas

Código de inicio

En los siguientes ejemplos, tomarás una List que representará el menú de galletas de una panadería y usarás funciones de orden superior a fin de darle formato al menú de diferentes maneras.

Para comenzar, configura el código inicial.

  1. Ve al Playground de Kotlin.
  2. Arriba de la función main(), agrega la clase Cookie. Cada instancia de Cookie representa un elemento en el menú, que contiene un name, un price y otra información sobre la galleta.
class Cookie(
    val name: String,
    val softBaked: Boolean,
    val hasFilling: Boolean,
    val price: Double
)

fun main() {

}
  1. Debajo de la clase Cookie, fuera de main(), crea una lista de galletas como se muestra. Se infiere que el tipo es 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() {

}

Cómo aplicar un bucle a una lista con forEach()

La primera función de orden superior que aprenderás es forEach(). forEach() ejecuta la función pasada como parámetro una vez por cada elemento de la colección. Esto funciona de manera similar a la función repeat() o a un bucle for. La lambda se ejecuta para el primer elemento, luego para el segundo, y así sucesivamente, hasta que se haya ejecutado para cada elemento de la colección. La firma del método es la siguiente:

forEach(action: (T) -> Unit)

forEach() toma un solo parámetro de acción: una función de tipo (T) -> Unit.

T corresponde a cualquier tipo de datos que contenga la colección. Debido a que la lambda toma un solo parámetro, puedes omitir el nombre y hacer referencia al parámetro con it.

Usa la función forEach() para imprimir los elementos de la lista cookies.

  1. En main(), llama a forEach() en la lista cookies mediante la sintaxis lambda final. Debido a que la lambda final es el único argumento, puedes omitir los paréntesis cuando llamas a la función.
fun main() {
    cookies.forEach {

    }
}
  1. En el cuerpo de la lambda, agrega una sentencia println() que imprima it.
fun main() {
    cookies.forEach {
        println("Menu item: $it")
    }
}
  1. Ejecuta tu código y observa el resultado. Todo lo que imprime es el nombre del tipo (Cookie) y un identificador único para el objeto, pero no el contenido del objeto.
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

Cómo incorporar expresiones en cadenas

Cuando te presentamos por primera vez las plantillas de cadenas, viste cómo el símbolo de dólar ($) se podía usar con un nombre de variable para insertarlo en una cadena. Sin embargo, esto no funciona como se espera cuando se combina con el operador de punto (.) para acceder a propiedades.

  1. En la llamada a forEach(), modifica el cuerpo de la expresión lambda para insertar $it.name en la string.
cookies.forEach {
    println("Menu item: $it.name")
}
  1. Ejecuta tu código. Observa que, con esta acción, se inserta el nombre de la clase, Cookie, y un identificador único del objeto, seguido de .name. No se accede al valor de la propiedad name.
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 acceder a las propiedades y, luego, incorporarlas en una cadena, necesitas una expresión. Puedes hacer que una expresión sea parte de una plantilla de cadena si la encierras entre llaves.

2c008744cee548cc.png

La expresión lambda se coloca entre las llaves de apertura y cierre. Puedes acceder a propiedades, realizar operaciones matemáticas, llamar a funciones, etc., y el valor que se muestra de la expresión lambda se inserta en la string.

Modifiquemos el código de modo que el nombre se inserte en la string.

  1. Encierra it.name entre llaves para convertirla en una expresión lambda.
cookies.forEach {
    println("Menu item: ${it.name}")
}
  1. Ejecuta tu código. El resultado contiene el 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. map()

La función map() te permite transformar una colección en otra nueva con la misma cantidad de elementos. Por ejemplo: map() podría transformar una List<Cookie> en una List<String> que solo contenga el name de la galleta, siempre que le indiques a la función map() cómo crear una String de cada elemento Cookie.

e0605b7b09f91717.png

Supongamos que escribes una app que muestra el menú interactivo de una panadería. Cuando el usuario navegue a la pantalla en la que se muestra el menú de galletas, es posible que desee ver los datos presentados de forma lógica, como el nombre seguido del precio. Puedes crear una lista de strings con formato de modo que incluya los datos relevantes (nombre y precio) mediante la función map().

  1. Quita todo el código anterior de main(). Crea una variable nueva llamada fullMenu y establécela en el resultado de llamar a map() en la lista cookies.
val fullMenu = cookies.map {

}
  1. En el cuerpo de la lambda, agrega una string con formato de modo que incluya el name y el price de it.
val fullMenu = cookies.map {
    "${it.name} - $${it.price}"
}
  1. Imprime el contenido de fullMenu. Puedes hacerlo mediante forEach(). La colección fullMenu que se muestra a partir de map() tiene el tipo List<String> en lugar de List<Cookie>. Cada Cookie en cookies corresponde a una String en fullMenu.
println("Full menu:")
fullMenu.forEach {
    println(it)
}
  1. Ejecuta tu código. El resultado coincide con el contenido de la lista 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. filter()

La función filter() te permite crear un subconjunto de una colección. Por ejemplo, si tuvieras una lista de números, podrías usar filter() para crear una lista nueva que solo contenga números divisibles por 2.

d4fd6be7bef37ab3.png

Mientras que el resultado de la función map() siempre genera una colección del mismo tamaño, filter() proporciona una colección del mismo tamaño o uno más pequeño que el de la colección original. A diferencia de map(), la colección resultante también tiene el mismo tipo de datos, por lo que filtrar una List<Cookie> generará otra List<Cookie>.

Al igual que las funciones map() y forEach(), filter() toma una sola expresión lambda como parámetro. La lambda tiene un solo parámetro que representa cada elemento de la colección y muestra un valor de tipo Boolean.

Para cada elemento de la colección, se cumple lo siguiente:

  • Si el resultado de la expresión lambda es true, el elemento se incluirá en la colección nueva.
  • Si el resultado es false, el elemento no se incluirá en la colección nueva.

Esto resulta útil si deseas obtener un subconjunto de datos en la app. Por ejemplo, supongamos que la panadería quiere destacar sus galletas blandas en una sección separada del menú. Antes de imprimir los elementos, puedes aplicar la función filter() a la lista de cookies.

  1. En main(), crea una nueva variable llamada softBakedMenu y establécela en el resultado de llamar a filter() en la lista de cookies.
val softBakedMenu = cookies.filter {
}
  1. En el cuerpo de la lambda, agrega una expresión booleana de modo que verifique si la propiedad softBaked de la galleta resulta true. Como softBaked es un Boolean en sí, el cuerpo de la lambda solo debe contener it.softBaked.
val softBakedMenu = cookies.filter {
    it.softBaked
}
  1. Imprime el contenido de softBakedMenu con forEach().
println("Soft cookies:")
softBakedMenu.forEach {
    println("${it.name} - $${it.price}")
}
  1. Ejecuta tu código. El menú se imprime como antes, pero solo incluye las galletas blandas.
...
Soft cookies:
Banana Walnut - $1.49
Snickerdoodle - $1.39
Blueberry Tart - $1.79

5. groupBy()

La función groupBy() se puede usar para convertir una lista en un mapa con base en una función. Cada valor único que se muestra de la función se convierte en una clave en el mapa resultante. Los valores de cada clave son todos los elementos de la colección que produjeron ese valor único.

54e190b34d9921c0.png

El tipo de datos de las claves coincide con el que se muestra de la función que se pasó a groupBy(). El tipo de datos de los valores es una lista de elementos de la lista original.

Esto puede resultar difícil de conceptualizar. Comencemos con un ejemplo simple. Dada la misma lista de números que antes, agrúpalos en impares o pares.

Puedes verificar si un número es impar o par dividiéndolo por 2 y comprobando si el resto es 0 o 1. Si el resto es 0, el número es par. De lo contrario, si el resto es 1, el número es impar.

Esto se puede lograr con el operador de módulo (%), que divide el dividendo en el lado izquierdo de una expresión por el divisor del lado derecho.

4c3333da9e5ee352.png

En lugar de mostrar el resultado de la división, como hace el operador de división (/), el operador de módulo muestra el resto. Esto lo hace útil para verificar si un número es par o impar.

4219eacdaca33f1d.png

Se llama a la función groupBy() con la siguiente expresión lambda: { it % 2 }.

El mapa resultante tiene dos claves: 0 y 1. Cada clave tiene un valor de tipo List<Int>. La lista para la clave 0 contiene todos los números pares, y aquella para la clave 1 contiene todos los impares.

Un caso de uso del mundo real podría ser una app de fotos que agrupa fotos según el tema o la ubicación donde se tomaron. Para nuestro menú de panadería, agrupemos el menú en función de si una galleta es blanda o no.

Usa la función groupBy() a fin de agrupar el menú según la propiedad softBaked.

  1. Quita la llamada a filter() del paso anterior.

Código para quitar

val softBakedMenu = cookies.filter {
    it.softBaked
}
println("Soft cookies:")
softBakedMenu.forEach {
    println("${it.name} - $${it.price}")
}
  1. Llama a la función groupBy() en la lista de cookies y almacena el resultado en una variable llamada groupedMenu.
val groupedMenu = cookies.groupBy {}
  1. Pasa una expresión lambda que muestre it.softBaked. El tipo de datos que se mostrará será Map<Boolean, List<Cookie>>.
val groupedMenu = cookies.groupBy { it.softBaked }
  1. Crea una variable softBakedMenu que contenga el valor de groupedMenu[true] y una variable crunchyMenu que contenga el valor de groupedMenu[false]. Como el resultado de suscribir un elemento Map puede ser nulo, puedes usar el operador Elvis (?:) para mostrar una lista vacía.
val softBakedMenu = groupedMenu[true] ?: listOf()
val crunchyMenu = groupedMenu[false] ?: listOf()
  1. Agrega código con el fin de imprimir el menú de las galletas blandas, seguido del menú de las galletas crujientes.
println("Soft cookies:")
softBakedMenu.forEach {
    println("${it.name} - $${it.price}")
}
println("Crunchy cookies:")
crunchyMenu.forEach {
    println("${it.name} - $${it.price}")
}
  1. Ejecuta tu código. Con la función groupBy(), dividirás la lista en dos, según el valor de una de las propiedades.
...
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. fold()

La función fold() se usa para generar un valor único a partir de una colección. Por lo general, se usa para calcular un total de precios o sumar todos los elementos de una lista para encontrar un promedio.

a9e11a1aad05cb2f.png

La función fold() toma dos parámetros:

  • Un valor inicial; el tipo de datos se infiere cuando se llama a la función (es decir, se deduce que un valor inicial de 0 es un Int)
  • Una expresión lambda que muestra un valor con el mismo tipo que el valor inicial

La expresión lambda tiene dos parámetros adicionales:

  • El primero se conoce como acumulador. Tiene el mismo tipo de datos que el valor inicial. Piensa en esto como un total acumulado. Cada vez que se llama a la expresión lambda, el acumulador equivaldrá al valor que se mostró cuando se llamó a la expresión lambda por última vez.
  • El segundo es el mismo tipo que cada elemento de la colección.

Al igual que con otras funciones que viste, se llama a la expresión lambda para cada elemento de una colección, de modo que puedes usar fold() como una forma concisa de sumar todos los elementos.

Usemos fold() para calcular el precio total de todas las galletas.

  1. En main(), crea una nueva variable llamada totalPrice y establécela en el resultado de llamar a fold() en la lista de cookies. Pasa 0.0 como valor inicial. Se infiere que el tipo es Double.
val totalPrice = cookies.fold(0.0) {
}
  1. Deberás especificar ambos parámetros para la expresión lambda. Usa total para el acumulador y cookie para el elemento de la colección. Usa la flecha (->) después de la lista de parámetros.
val totalPrice = cookies.fold(0.0) {total, cookie ->
}
  1. En el cuerpo de la lambda, calcula la suma de total y cookie.price. Se infiere que es el valor que se muestra y se pasará como el total la próxima vez que se llame a la lambda.
val totalPrice = cookies.fold(0.0) {total, cookie ->
    total + cookie.price
}
  1. Imprime el valor de totalPrice, con el formato de una string para facilitar la lectura.
println("Total price: $${totalPrice}")
  1. Ejecuta tu código. El resultado debe ser igual a la suma de los precios de la lista de cookies.
...
Total price: $10.83

7. sortedBy()

Cuando viste por primera vez las colecciones, aprendiste que la función sort() se podía usar para ordenar los elementos. Sin embargo, esto no funcionará en una colección de objetos Cookie. La clase Cookie tiene varias propiedades, y Kotlin no sabrá cuáles (name, price, etc.) quieres usar para ordenar.

En estos casos, las colecciones de Kotlin proporcionan una función sortedBy(). sortedBy() te permite especificar una expresión lambda que muestra la propiedad según la cual deseas ordenar. Por ejemplo, si deseas ordenar por price, la lambda mostrará it.price. Siempre y cuando el tipo de datos del valor tenga un orden natural (las cadenas se ordenan alfabéticamente y los valores numéricos se ordenan de forma ascendente), se ordenará como si se tratara de colecciones de ese tipo.

5fce4a067d372880.png

Usarás sortedBy() para ordenar la lista de galletas alfabéticamente.

  1. En main(), después del código existente, agrega una variable nueva llamada alphabeticalMenu y configúrala para llamar a sortedBy() en la lista de cookies.
val alphabeticalMenu = cookies.sortedBy {
}
  1. En la expresión lambda, muestra it.name. La lista resultante seguirá siendo de tipo List<Cookie>, pero se ordenará según el name.
val alphabeticalMenu = cookies.sortedBy {
    it.name
}
  1. Imprime los nombres de las galletas en alphabeticalMenu. Puedes usar la función forEach() para imprimir cada nombre en una línea nueva.
println("Alphabetical menu:")
alphabeticalMenu.forEach {
    println(it.name)
}
  1. Ejecuta tu código. Los nombres de las galletas están impresos en orden alfabético.
...
Alphabetical menu:
Banana Walnut
Blueberry Tart
Chocolate Chip
Chocolate Peanut Butter
Snickerdoodle
Sugar and Sprinkles
Vanilla Creme

8. Conclusión

¡Felicitaciones! Acabas de ver varios ejemplos de cómo se pueden usar las funciones de orden superior con las colecciones. Las operaciones comunes, como ordenar y filtrar, se pueden realizar en una sola línea de código, lo que hace que tus programas sean más concisos y expresivos.

Resumen

  • Puedes aplicar un bucle a cada elemento de una colección conforEach().
  • Las expresiones se pueden insertar en cadenas.
  • map() se usa para darle formato a los elementos de una colección, a menudo como una colección de otro tipo de datos.
  • filter() puede generar un subconjunto de una colección.
  • groupBy() divide una colección según un valor que se muestra de una función.
  • fold() convierte una colección en un solo valor.
  • sortedBy() se usa para ordenar una colección según una propiedad especificada.

9. Más información