Saltar al contenido
K
Kotlin: el lenguajePaso a paso32 min de lectura · 5 min de máquina Intermedio

Lambdas con listas: filter, map y reglas

Al terminar: Usar lambdas con filter, filterIsInstance y map, y guardar reglas como funciones en el torneo.

Al terminar esta lección podrás preguntarle a un partido cosas como «dame solo las tarjetas», «dame los goleadores» o «cuántos eventos hubo en la primera mitad», y cada pregunta será una línea. Es lo que se consigue al juntar las lambdas de la lección anterior con las listas: filter para quedarte con unos elementos, map para transformarlos y filterIsInstance para quedarte con los de un tipo.

Temas: filter, map, filterIsInstance, el atajo :: (las referencias) y reglas guardadas en variables que se pasan a filter.

filter y map: dos preguntas a una lista

Una lista es un valor que guarda varios valores en orden (listOf(4, 1, 7)); las listas se estudian a fondo en Colecciones. Hoy solo necesitas dos funciones suyas que reciben una lambda y la aplican a cada elemento:

  • filter { … } se queda con los elementos para los que la lambda da true y devuelve una lista nueva con ellos.
  • map { … } transforma cada elemento con la lambda y devuelve la lista de los resultados, en el mismo orden.

Ninguna cambia la lista original: devuelven otra. Se pueden encadenar, porque cada una devuelve una lista. Y en lugar de la lambda puedes pasar una referencia con :: si solo llama a algo que ya existe (la sección siguiente a este ejemplo lo explica).

ejemplo mínimo: filter, map y filterIsInstance (con comentarios)
fun main() {
    val precios = listOf(4, 1, 7, 2)
 
    // filter: se queda con los elementos para los que la lambda da true
    println(precios.filter { it > 2 })
 
    // map: transforma cada elemento con la lambda y devuelve la lista de resultados
    println(precios.map { it * 10 })
 
    // se encadenan: primero filter, luego map sobre lo que quedó
    println(precios.filter { it > 2 }.map { it * 10 })
 
    // map también recibe una referencia con :: en lugar de la lambda.
    // length es el número de letras de un texto
    val palabras = listOf("pera", "kiwi", "uva")
    println(palabras.map(String::length))   // String::length equivale a { it.length }
 
    // filterIsInstance<String>(): entre los <> va el tipo que quieres conservar.
    // Esta lista admite cualquier cosa: mezcla números y textos
    val mezcla = listOf(1, "dos", 3, "cuatro")
    println(mezcla.filterIsInstance<String>())
}
[4, 7]
[40, 10, 70, 20]
[40, 70]
[4, 4, 3]
[dos, cuatro]

filterIsInstance<Tipo>() es lo que usarás con los eventos del partido: de una lista de EventoDePartido te quedas solo con los Gol, y el resultado ya es una lista de Gol, así que puedes usar sus propiedades sin más comprobaciones.

El atajo ::

Si la lambda solo llama a algo que ya existe, puedes nombrarlo directamente con :: y te ahorras escribir la lambda. Según lo que haya a la izquierda de :::

  • Un tipo: String::length equivale a { texto -> texto.length }. Es la propiedad de cualquier String, y la lambda recibe ese String.
  • Nada, para una función suelta (no está dentro de ninguna clase): ::nombreDe equivale a { producto -> nombreDe(producto) }.
  • Un objeto concreto: partido::golesDe equivale a { equipo -> partido.golesDe(equipo) }, ligada a ese partido. No la necesitas en esta lección.

El ejemplo de la cesta, justo debajo, usa las dos primeras.

Un ejemplo con una cesta de la compra

Ahora, todo junto con otros datos: una cesta con frutas y pan, que son dos tipos de Producto (una interfaz sellada, como la de Sealed: estados con datos). La clase Cesta tiene una función filtrar que recibe la condición como lambda, igual que lo hará Partido. Y esPan es una condición guardada en una variable, que se pasa a filtrar como cualquier otra función:

ejemplo mínimo: una cesta que se filtra con lambdas (con comentarios)
sealed interface Producto
data class Fruta(val nombre: String, val precio: Int) : Producto
data class Pan(val precio: Int) : Producto
 
// Función suelta: el nombre de cualquier producto (when exhaustivo sobre la interfaz sellada)
fun nombreDe(producto: Producto): String = when (producto) {
    is Fruta -> producto.nombre
    is Pan -> "Pan"
}
 
class Cesta(val productos: List<Producto>) {
    // Función de orden superior: recibe una condición y la pasa a filter
    fun filtrar(condicion: (Producto) -> Boolean): List<Producto> = productos.filter(condicion)
}
 
// Una regla guardada en una variable: se reutiliza donde haga falta
val esPan: (Producto) -> Boolean = { it is Pan }
 
fun main() {
    val cesta = Cesta(listOf(Fruta("Pera", 2), Pan(1), Fruta("Mango", 4)))
 
    println(cesta.filtrar { it is Pan })              // trailing lambda con it
    println(cesta.filtrar(esPan).size)                // la misma regla, guardada y pasada por su nombre
 
    val frutas = cesta.productos.filterIsInstance<Fruta>()   // devuelve List<Fruta>
    println(frutas.map(Fruta::nombre))                // Tipo::propiedad = { f -> f.nombre }
    println(frutas.filter { it.precio <= 2 })         // aquí it es una Fruta: tiene precio
    println(cesta.productos.map(::nombreDe))          // ::funcion = { p -> nombreDe(p) }
}
[Pan(precio=1)]
1
[Pera, Mango]
[Fruta(nombre=Pera, precio=2)]
[Pera, Pan, Mango]

Reto · Ejercicio: palabras y mezclas

En el Kotlin Playground, fuera de tu proyecto. Con la lista de palabras sol, luna, estrella y mar, imprime, en este orden: las palabras de más de 3 letras; las cuatro palabras en mayúsculas (uppercase() convierte un texto a mayúsculas); y la longitud de las palabras de más de 3 letras (length es el número de letras de un texto). Después, con la lista mezclada 3, «casa», 8, «tren» y 5, imprime los números multiplicados por 2 (solo los números).

La consola debe mostrar:

[luna, estrella]
[SOL, LUNA, ESTRELLA, MAR]
[4, 8]
[6, 16, 10]
Ver solución
solución: filter, map y filterIsInstance
fun main() {
    val palabras = listOf("sol", "luna", "estrella", "mar")
    println(palabras.filter { it.length > 3 })                  // las de más de 3 letras
    println(palabras.map { it.uppercase() })                    // todas, en mayúsculas
    println(palabras.filter { it.length > 3 }.map { it.length })  // primero filtra, luego mide
 
    val mezcla = listOf(3, "casa", 8, "tren", 5)
    // filterIsInstance<Int>() deja solo los números; map multiplica cada uno
    println(mezcla.filterIsInstance<Int>().map { it * 2 })
}

Paso · Preguntarle cosas al partido

Objetivo. Añadir a Partido las funciones eventos(filtro) y goleadores(), completar Main con tres líneas más y copiar LambdasTest.kt. Al terminar, ./gradlew :torneo:run imprime seis líneas de lambdas y los tests de la lección pasan.

torneo/
kotlin+java/
com.sebastiannarvaez.marcador.torneo
Partido.ktse modifica
Main.ktse modifica
com.sebastiannarvaez.marcador.torneo (test)
LambdasTest.ktse crea (se copia)
Lo que tocas, en la vista Android: dos modificados y el test de esta lección.

Partido.kt y Main.kt ya existen: ábrelos con doble clic. Una rareza de Partido: ya tiene una propiedad llamada eventos (la lista) y ahora tendrá también una función eventos(filtro). Kotlin las distingue por los paréntesis: partido.eventos es la lista y partido.eventos { … } llama a la función. Es la misma idea que la filtrar de la cesta, solo que con el mismo nombre.

Dónde va cada cosa. En Partido.kt, pega las dos funciones nuevas justo encima del comentario // Retorno con etiqueta (la función minutosDeGolDe de la lección anterior queda debajo, sin cambios); el … del bloque solo marca el código que ya tienes. En Main.kt, pega el bloque nuevo en lugar del comentario // Lambdas: minutos de gol y repetir.: trae un comentario nuevo y las tres líneas, y debajo siguen Minutos de gol de Ana y repetir, sin cambios.

Intenta escribirlo tú, en este orden:

  1. Dos preguntas a Partido. eventos(filtro) recibe una condición sobre un evento y devuelve los eventos que la cumplen; goleadores() devuelve quién marcó cada gol, en orden (si prefieres, escríbela con un bucle for y mutableListOf<Jugador>() en vez de filterIsInstance y map: el resultado es el mismo).
  2. main. Con el partido de la lección anterior: describe las tarjetas (con describir), cuenta los eventos de la primera mitad (antes del minuto 45) y lista los goleadores por nombre.

Tras las líneas de la lección anterior, debe verse:

Tarjetas: [30' Tarjeta amarilla para Iván]
Primera mitad: 2 eventos
Goleadores: [Ana, Iván, Ana]
Minutos de gol de Ana: [12, 80]
Vuelta 1
Vuelta 2

Cuando termines, abre el bloque y compara con tu código.

Intenta primero. Cuando termines, compara con la solución.Mostrar el código
torneo/src/main/java/com/sebastiannarvaez/marcador/torneo/Partido.kt
    …
 
    // Función de orden superior: recibe una función como parámetro.
    // `(EventoDePartido) -> Boolean` es un tipo función: toma un evento y
    // devuelve si cumple. Al llamarla, la lambda va fuera de los paréntesis
    // (trailing lambda): partido.eventos { it is Tarjeta }
    fun eventos(filtro: (EventoDePartido) -> Boolean): List<EventoDePartido> = eventos.filter(filtro)
 
    // `Gol::jugador` es una referencia a propiedad: equivale a { gol -> gol.jugador }.
    fun goleadores(): List<Jugador> = eventos.filterIsInstance<Gol>().map(Gol::jugador)
 
    // Retorno con etiqueta: `return@forEach` sale solo de la lambda actual
    …
torneo/src/main/java/com/sebastiannarvaez/marcador/torneo/Main.kt
    // Lambdas: filtrar eventos y sacar goleadores.
    println("Tarjetas: ${alFinal.eventos { it is Tarjeta }.map(::describir)}")
    println("Primera mitad: ${alFinal.eventos(antesDelMinuto(45)).size} eventos")
    println("Goleadores: ${alFinal.goleadores().map { it.nombre }}")
    …

::describir es la referencia a una función suelta: apunta a la función describir de Sealed: estados con datos. Y map { it.nombre } es una lambda con it.

Ejecuta ./gradlew :torneo:run en la Terminal de Android Studio (View → Tool Windows → Terminal). Las seis últimas líneas son las del punto de control de abajo.

El test

LambdasTest.kt son seis tests de las funciones que has escrito en esta lección y en la anterior; no enseñan nada nuevo de kotlin.test, así que no los escribes: los copias. Primero crea el fichero: en torneo → kotlin+java, clic derecho sobre el paquete com.sebastiannarvaez.marcador.torneo (test) (el que lleva (test) detrás) → New → Kotlin Class/File → File → LambdasTest → Intro. Después pulsa Copiar fichero, que copia el contenido al portapapeles. En el editor, selecciona todo (Cmd+A) y pega (Cmd+V): el fichero copiado trae su propia línea package, y sustituye a la que escribió Android Studio.

LambdasTest.kt

Del fichero que copiaste, este es el test que enseña algo: usa la función que devuelve función. (Se muestra solo el test, sin la clase que lo rodea.)

un test de LambdasTest, resumido
@Test
fun funcionQueDevuelveFuncion() {
    assertEquals(1, partido.eventos(antesDelMinuto(20)).size)
    assertEquals(3, partido.eventos(antesDelMinuto(60)).size)
}

Ejecuta ./gradlew :torneo:test. Debe terminar en BUILD SUCCESSFUL. Para ver el recuento, abre el informe como en Bucles y tu primer test: open torneo/build/reports/tests/test/index.html en la Terminal; arriba debe poner 36 tests.

Punto de control

  • ./gradlew :torneo:run termina en BUILD SUCCESSFUL y las últimas líneas son:
Tarjetas: [30' Tarjeta amarilla para Iván]
Primera mitad: 2 eventos
Goleadores: [Ana, Iván, Ana]
Minutos de gol de Ana: [12, 80]
Vuelta 1
Vuelta 2
  • ./gradlew :torneo:test pasa. El informe (open torneo/build/reports/tests/test/index.html) muestra 36 tests en total: los 30 de antes y los 6 de LambdasTest.
  • Partido tiene a la vez la propiedad eventos y la función eventos(filtro) y compila.
  • En minutosDeGolDe usas return@forEach, no return. Cámbialo por return minutos, ejecuta ./gradlew :torneo:test y comprueba que minutosDeGolSaltaLosEventosQueNoSonGoles falla. Devuélvelo.

Comprueba lo que entendiste

5 preguntas · necesitas 4 para completar la lección

  1. 1.¿Qué devuelve listOf(4, 1, 7, 2).filter { it > 2 }?

  2. 2.Tienes val precios = listOf(4, 1, 7, 2). ¿Qué devuelve precios.map { it * 10 }?

  3. 3.Con una lista de EventoDePartido, ¿para qué sirve filterIsInstance<Gol>()?

  4. 4.En Partido conviven la propiedad eventos y la función eventos(filtro). ¿Cómo sabe Kotlin a cuál te refieres?

  5. 5.Escribes partido.eventos(antesDelMinuto(45)). ¿Qué es antesDelMinuto(45)?

Faltan 5 de 5