Lambdas y funciones de orden superior
Al terminar: Escribir lambdas, pasar funciones como parámetros y leer una trailing lambda sin dudar.
Al terminar esta lección sabrás pasar un trozo de código como si fuera un dato: eso es una lambda. Con ella Marcador podrá preguntarle a un partido «dame los minutos de gol de Ana» y repetir una acción las veces que se pida. En la lección siguiente, Lambdas con listas, las usarás para filtrar y transformar listas de eventos.
Es la sintaxis que más vas a leer en Kotlin y en Android. Casi todo lo que sigue en el curso —colecciones, pantallas— recibe lambdas. Temas: lambdas, tipos función, funciones de orden superior, trailing lambda, it, captura y retorno con etiqueta. Los atajos con :: llegan en la lección siguiente.
Una función que se guarda y se pasa
Hasta ahora, una función era algo que llamabas: puntosPor(2, 1). Desde hoy, una función también puede ser un valor, igual que un número o un texto: la puedes guardar en una variable y pasársela a otra función. ¿Para qué? Para separar «qué recorrer» de «qué hacer con cada elemento». Imagina que quieres «dame solo las tarjetas», «dame solo los goles» o «dame los eventos antes del minuto 45». Los tres recorren la lista de eventos igual; solo cambia la pregunta. Con funciones como valores, escribes el recorrido una sola vez y le pasas la pregunta como un dato. (Cómo se hace con listas lo verás en la lección siguiente; hoy el foco son las lambdas.)
Una lambda es una función sin nombre que se escribe donde se necesita: se declara sin fun, entre llaves. Los parámetros van a la izquierda de una flecha -> y el cuerpo a la derecha, y la última expresión del cuerpo es lo que devuelve.
Guardar una lambda en una variable
El tipo de una lambda se escribe como una firma (la forma de una función: qué recibe y qué devuelve): (Int) -> Int significa «recibe un Int y devuelve un Int». Lo de la izquierda de la flecha son los parámetros y lo de la derecha, lo que devuelve. Una lambda guardada en una val se llama como cualquier función.
fun main() {
// Una lambda: va entre llaves; a la izquierda de la flecha, los parámetros; a la derecha, el cuerpo.
// El tipo (Int) -> Int dice: recibe un Int y devuelve un Int.
val conDescuento: (Int) -> Int = { precio -> precio - 5 }
// Con un solo parámetro se puede omitir su nombre: se llama it
val conIva: (Int) -> Int = { it + it / 10 }
// Una lambda sin parámetros no lleva flecha
val saludo = { println("Bienvenido a la tienda") }
println(conDescuento(40)) // se llama como cualquier función
println(conIva(100))
saludo()
}35
110
Bienvenido a la tiendaPasar una lambda a otra función
Una función que recibe o devuelve otra función se llama de orden superior. El parámetro que recibe la función lleva su tipo función, y dentro se usa como una función más.
val conDescuento: (Int) -> Int = { precio -> precio - 5 }
// Función de orden superior: recibe otra función como parámetro y la usa
fun aplicar(valor: Int, regla: (Int) -> Int): Int = regla(valor)
fun main() {
println(aplicar(40, conDescuento)) // le pasas una lambda ya guardada en una val
println(aplicar(40, { it * 2 })) // o la escribes en el sitio, entre las llaves
println(aplicar(40) { it * 3 }) // trailing lambda: como es el último parámetro, sale de los paréntesis
}35
80
120aplicar(40) { it * 3 } es la trailing lambda: como la lambda es el último parámetro, sale de los paréntesis. Si es el único argumento, los paréntesis desaparecen: lista.filter { it > 0 } (filter es una función de las listas que se queda con los elementos para los que la lambda da true; se explica en Lambdas con listas). Lo verás en cada pantalla del curso.
Una función que devuelve una lambda, y lambdas con dos parámetros
Una función de orden superior también puede devolver una lambda. Se escribe el tipo función después de los dos puntos de la función y, a la derecha del =, la lambda. Además, una lambda puede tener dos parámetros: van con nombre, separados por coma, antes de la flecha. Y un tipo función puede devolver Unit, que significa «no devuelve nada útil» (es lo que devuelve println). Todo en un ejemplo con precios:
// Lambda con DOS parámetros: van con nombre, separados por coma, antes de la flecha
val masCaroQue: (Int, Int) -> Boolean = { precio, limite -> precio > limite }
// Una función que DEVUELVE una lambda: el tipo de lo que devuelve es (Int) -> Boolean
// y la lambda va a la derecha del =. Esa lambda recuerda `tope` (captura, se explica abajo).
fun baratoHasta(tope: Int): (Int) -> Boolean = { it <= tope }
// (Int) -> Unit: la función recibida toma un Int y no devuelve nada útil
fun contarHasta(n: Int, accion: (Int) -> Unit) {
for (i in 1..n) accion(i) // se llama a la función recibida como a cualquier otra
}
fun main() {
println(masCaroQue(3, 5)) // una lambda se llama como una función
val esBarato = baratoHasta(2) // esBarato es la lambda que devolvió baratoHasta
println(esBarato(1))
println(esBarato(4))
contarHasta(2) { n -> println("Paso $n") } // la lambda recibe cada número
}false
true
false
Paso 1
Paso 2Capturar una variable de fuera
Una lambda puede usar, e incluso cambiar, variables que existían donde se escribió. Eso es capturar, y la lambda se las lleva consigo aunque se ejecute más tarde:
fun main() {
var ventas = 0 // una variable de fuera de la lambda
val vender = { ventas += 1 } // la lambda usa y cambia esa variable: la captura
vender()
vender()
println(ventas) // la lambda cambió la variable de fuera
}2Retorno con etiqueta
Dentro de una lambda, return@forEach sale solo de la lambda. (forEach es la función de las listas que ejecuta una lambda para cada elemento; lo usaste en Main.kt.) Así forEach sigue con el elemento siguiente: es la forma de decir «salta este elemento». Un return a secas, en cambio, sale de la función entera que contiene la lambda. Es justo el error de la sección siguiente.
Error típico: el return que se lleva la función entera
Quieres saltar los eventos que no son goles. Escribes return para saltarte esos eventos. Dos piezas nuevas en el ejemplo: mutableListOf<Int>() crea una lista de enteros a la que se puede añadir con add (entre los <> va el tipo de lo que guarda la lista), y evento !is Gol pregunta «¿este evento NO es un Gol?».
fun minutosDeGolDe(jugador: Jugador): List<Int> {
val minutos = mutableListOf<Int>()
eventos.forEach { evento ->
if (evento !is Gol) return minutos // sale de minutosDeGolDe, no salta el evento
if (evento.jugador == jugador) minutos.add(evento.minuto)
}
return minutos
}Compila y no da error. Con los eventos gol de Ana (10'), tarjeta (30'), gol de Luis (50') y gol de Ana (70'), devuelve [10] y no [10, 70]: en cuanto aparece la tarjeta, return termina la función entera y los goles del 70' nunca se miran.
fun minutosDeGolDe(jugador: Jugador): List<Int> {
val minutos = mutableListOf<Int>()
eventos.forEach { evento ->
if (evento !is Gol) return@forEach // salta solo este evento
if (evento.jugador == jugador) minutos.add(evento.minuto)
}
return minutos
}En el módulo de Swift verás su equivalente, que allí se llama closure.
Reto · Ejercicio: reglas como lambdas
En el Kotlin Playground: escribe una función transformar(numeros, regla) que aplique una regla a cada número de una lista y devuelva la lista de resultados. Recórrela con for (n in numeros), que toma uno a uno los elementos de la lista, y guarda los resultados en mutableListOf<Int>(), a la que se añade con add. Úsala sobre los números 1, 2 y 3 con tres reglas: sumar uno (sin nombrar el parámetro), duplicar (nombrándolo) y multiplicar por diez (con una función multiplicadorPor(factor) que devuelve la regla, como baratoHasta). Después, explica por qué la última «recuerda» el 10. La consola debe mostrar:
[2, 3, 4]
[2, 4, 6]
[10, 20, 30]Ver solución
fun transformar(numeros: List<Int>, regla: (Int) -> Int): List<Int> {
val resultado = mutableListOf<Int>()
for (n in numeros) resultado.add(regla(n)) // for (n in lista): n toma cada elemento, uno por vuelta
return resultado
}
fun multiplicadorPor(factor: Int): (Int) -> Int = { it * factor }
fun main() {
val datos = listOf(1, 2, 3)
println(transformar(datos) { it + 1 }) // [2, 3, 4]
println(transformar(datos) { n -> n * 2 }) // [2, 4, 6]
println(transformar(datos, multiplicadorPor(10))) // [10, 20, 30]
}La última línea imprime [10, 20, 30]. La lambda que devuelve multiplicadorPor captura factor: cuando multiplicadorPor termina, su parámetro factor deja de existir como variable local, pero la lambda conserva el valor 10 y lo usa cada vez que se llama.
Paso · Las primeras lambdas en el torneo
Objetivo. Crear Lambdas.kt con cuatro piezas sueltas, añadir a Partido la función minutosDeGolDe(jugador) (con return@forEach) y ver el resultado en Main. Al terminar, ./gradlew :torneo:run imprime tres líneas nuevas. Filtrar y transformar listas de eventos llega en la lección siguiente.
Crea Lambdas.kt. En la vista Android, despliega torneo → kotlin+java. Haz clic derecho sobre el paquete com.sebastiannarvaez.marcador.torneo (el que no lleva (test) detrás) → New → Kotlin Class/File → File → escribe Lambdas y pulsa Intro.
Dónde va cada cosa. Lambdas.kt es un fichero nuevo entero. En Partido.kt, pega el bloque nuevo en lugar de la última } del fichero (la que cierra la clase, después de resultado(); el bloque ya trae esa llave al final); el resto del fichero no cambia, y el … del bloque solo marca el código que ya tienes. En Main.kt, pega las líneas nuevas en lugar de la última } de main (el bloque también la trae); el resto del fichero no cambia.
Intenta escribirlo tú, en este orden:
Lambdas.kt. Cuatro piezas sueltas:esGol, una condición guardada en una variable que dice si un evento es un gol (conit is Gol, que pregunta «¿este evento es unGol?»; el tipo común de los eventos esEventoDePartido);esDelMinutoOAntes, otra condición que recibe un evento y un minuto límite;repetir(veces, accion), que ejecuta una acción tantas veces como se pida y le pasa el número de vuelta; yantesDelMinuto(minuto), que devuelve una condición «el evento ocurrió antes de ese minuto».Partido.minutosDeGolDe(jugador)devuelve los minutos en que marcó ese jugador, recorriendoeventosconforEachy saltando conreturn@forEachlos eventos que no son goles.main. Con el partido de la lección anterior: los minutos de gol de Ana yrepetir(2)para imprimir «Vuelta 1» y «Vuelta 2».
Tras las líneas de la lección anterior, debe verse:
Minutos de gol de Ana: [12, 80]
Vuelta 1
Vuelta 2Cuando termines, abre el bloque y compara con tu código. Los bloques con ruta llevan la del fichero en tu proyecto.
Intenta primero. Cuando termines, compara con la solución.Mostrar el código
package com.sebastiannarvaez.marcador.torneo
// Una lambda guardada en una `val`, con su tipo función explícito.
// Con un solo parámetro, la lambda lo llama `it` si no se le da nombre.
val esGol: (EventoDePartido) -> Boolean = { it is Gol }
// Con nombre de parámetro explícito y varios parámetros.
val esDelMinutoOAntes: (EventoDePartido, Int) -> Boolean = { evento, limite -> evento.minuto <= limite }
// Función de orden superior propia: ejecuta `accion` tantas veces como se pida.
fun repetir(veces: Int, accion: (Int) -> Unit) {
for (i in 1..veces) accion(i)
}
// Una función puede DEVOLVER otra. El resultado «recuerda» `minuto` (captura).
fun antesDelMinuto(minuto: Int): (EventoDePartido) -> Boolean = { it.minuto < minuto } …
// Retorno con etiqueta: `return@forEach` sale solo de la lambda actual
// (salta ese evento), no de la función. Un `return` a secas saldría de la función.
fun minutosDeGolDe(jugador: Jugador): List<Int> {
val minutos = mutableListOf<Int>()
eventos.forEach { evento ->
if (evento !is Gol) return@forEach
if (evento.jugador == jugador) minutos.add(evento.minuto)
}
return minutos
}
} …
// Lambdas: minutos de gol y repetir.
println("Minutos de gol de Ana: ${alFinal.minutosDeGolDe(ana)}")
repetir(2) { vuelta -> println("Vuelta $vuelta") }
}repetir la usa main ahora y antesDelMinuto, en la lección siguiente; esGol y esDelMinutoOAntes solo las comprueban los tests de esa lección.
Ejecuta ./gradlew :torneo:run en la Terminal de Android Studio (View → Tool Windows → Terminal, que se abre en la carpeta del proyecto, donde está gradlew). Las tres últimas líneas son las del punto de control de abajo.
Punto de control
./gradlew :torneo:runtermina enBUILD SUCCESSFULy las últimas líneas son:
Minutos de gol de Ana: [12, 80]
Vuelta 1
Vuelta 2./gradlew :torneo:testpasa con los mismos 30 tests de antes: esta parte no añade ninguno. Los de las lambdas llegan en la lección siguiente.- En
minutosDeGolDeusasreturn@forEach, noreturn. Cámbialo porreturn minutos, ejecuta./gradlew :torneo:runy comprueba que ahora imprimeMinutos de gol de Ana: [12]: en cuanto aparece la tarjeta, la función entera termina. Devuélvelo areturn@forEach.
Comprueba lo que entendiste
6 preguntas · necesitas 5 para completar la lección
1.¿Qué significa el tipo
(EventoDePartido) -> Boolean?2.Tienes
fun repetir(veces: Int, accion: (Int) -> Unit). ¿Cuál de estas llamadas es equivalente arepetir(2, { println(it) })?3.Dentro de
eventos.forEach { … }escribesreturn minutospara saltar los eventos que no son goles. ¿Qué ocurre?4.En
val doble: (Int) -> Int = { it * 2 }, ¿qué esit?5.En una entrevista te preguntan qué es «capturar» una variable en una lambda. ¿Cuál es la respuesta correcta?
6.Tienes
fun baratoHasta(tope: Int): (Int) -> Boolean = { it <= tope }yval esBarato = baratoHasta(2). ¿Qué imprimeprintln(esBarato(4))?