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

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.

ejemplo mínimo: una lambda guardada en una variable (con comentarios)
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 tienda

Pasar 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.

ejemplo mínimo: una función que recibe una función (con comentarios)
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
120

aplicar(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:

ejemplo mínimo: dos parámetros, una función que devuelve una lambda y Unit (con comentarios)
// 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 2

Capturar 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:

ejemplo mínimo: una lambda que captura una variable (con comentarios)
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
}
2

Retorno 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?».

con el bug: return dentro de forEach
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.

corregido: return@forEach
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
solución: transformar con lambdas
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.

torneo/
kotlin+java/
com.sebastiannarvaez.marcador.torneo
Lambdas.ktse crea
Partido.ktse modifica
Main.ktse modifica
Lo que tocas, en la vista Android: uno nuevo y dos modificados.

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:

  1. Lambdas.kt. Cuatro piezas sueltas: esGol, una condición guardada en una variable que dice si un evento es un gol (con it is Gol, que pregunta «¿este evento es un Gol?»; el tipo común de los eventos es EventoDePartido); 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; y antesDelMinuto(minuto), que devuelve una condición «el evento ocurrió antes de ese minuto».
  2. Partido. minutosDeGolDe(jugador) devuelve los minutos en que marcó ese jugador, recorriendo eventos con forEach y saltando con return@forEach los eventos que no son goles.
  3. main. Con el partido de la lección anterior: los minutos de gol de Ana y repetir(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 2

Cuando 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
torneo/src/main/java/com/sebastiannarvaez/marcador/torneo/Lambdas.kt
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 }
torneo/src/main/java/com/sebastiannarvaez/marcador/torneo/Partido.kt
    …
 
    // 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
    }
}
torneo/src/main/java/com/sebastiannarvaez/marcador/torneo/Main.kt
    …
 
    // 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:run termina en BUILD SUCCESSFUL y las últimas líneas son:
Minutos de gol de Ana: [12, 80]
Vuelta 1
Vuelta 2
  • ./gradlew :torneo:test pasa con los mismos 30 tests de antes: esta parte no añade ninguno. Los de las lambdas llegan en la lección siguiente.
  • En minutosDeGolDe usas return@forEach, no return. Cámbialo por return minutos, ejecuta ./gradlew :torneo:run y comprueba que ahora imprime Minutos de gol de Ana: [12]: en cuanto aparece la tarjeta, la función entera termina. Devuélvelo a return@forEach.

Comprueba lo que entendiste

6 preguntas · necesitas 5 para completar la lección

  1. 1.¿Qué significa el tipo (EventoDePartido) -> Boolean?

  2. 2.Tienes fun repetir(veces: Int, accion: (Int) -> Unit). ¿Cuál de estas llamadas es equivalente a repetir(2, { println(it) })?

  3. 3.Dentro de eventos.forEach { … } escribes return minutos para saltar los eventos que no son goles. ¿Qué ocurre?

  4. 4.En val doble: (Int) -> Int = { it * 2 }, ¿qué es it?

  5. 5.En una entrevista te preguntan qué es «capturar» una variable en una lambda. ¿Cuál es la respuesta correcta?

  6. 6.Tienes fun baratoHasta(tope: Int): (Int) -> Boolean = { it <= tope } y val esBarato = baratoHasta(2). ¿Qué imprime println(esBarato(4))?

Faltan 6 de 6