Funciones lambda
⏱ Dedicación recomendada: 0 minutos
Esto considera el contenido visible y relevante, e ignora texto colapsado o marcado como opcional.
r8vnhill/functional-programming-kt
Comencemos por crear un módulo para la lección...
- Windows
- Windows (corto)
- Linux/Mac
New-Item -Path "lambdas" -ItemType "Directory"
"// Intentionally left blank" > "lambdas\build.gradle.kts"
mkdir "lambdas"
"// Intentionally left blank" > "lambdas\build.gradle.kts"
mkdir "lambdas"
echo "// Intentionally left blank" > "lambdas/build.gradle.kts"
Recuerda agregar el nuevo módulo al archivo settings.gradle.kts
.
Las funciones lambda, también conocidas como funciones anónimas, son una forma concisa y flexible de representar funciones sin un nombre explícito. Son especialmente útiles cuando necesitamos una función que se puede pasar como argumento, o cuando queremos escribir una función de manera breve sin necesidad de definirla previamente. En su forma más simple, una función lambda es un bloque de código sin nombre que puede ser asignado a una variable o pasado como parámetro a otras funciones, permitiendo una gran flexibilidad en la escritura de código.
Funciones anónimas en Kotlin
En Kotlin, las funciones anónimas y las lambdas son similares, pero no exactamente iguales. La diferencia radica principalmente en la sintaxis y en cómo se manejan ciertas características como los retornos. Sin embargo, ambas pueden usarse de manera intercambiable en la mayoría de los casos.
Las funciones anónimas tienen una sintaxis más "tradicional", similar a una función regular, y permite declarar el tipo de retorno. Ejemplo:
val sumar = fun(a: Int, b: Int): Int {
return a + b
}
En la práctica usar una u otra será un tema de preferencia, pero es más común ver lambdas en el código de Kotlin.
Definición
Una función lambda es una función sin nombre que puede tomar argumentos y devolver un valor. Es una instancia de un tipo de función que puede ser tratada como un valor. A menudo se usan para expresar bloques de código que se ejecutarán más adelante, como en el caso de funciones de orden superior que toman funciones como parámetros o la evaluación perezosa.
La sintaxis general de una función lambda en Kotlin es:
val lambdaName: (T, S, ...) -> R = { t, s, ... -> body }
Donde:
lambdaName
: Nombre de la variable que almacena la función lambda.T, S, ...
: Tipos de los argumentos de la función lambda (...
indica que puede haber más de un argumento, pero se presenta como pseudocódigo y no es sintáxis válida).R
: Tipo de retorno de la función lambda.t, s, ...
: Nombres de los argumentos de la función lambda (nuevamente...
indica que puede haber más de un argumento, pero es pseudocódigo).body
: Cuerpo de la función lambda.
Si quieres crear el archivo desde la terminal...
- Windows
- Windows (corto)
- Linux/Mac
$Group = "com\github\username"
$LambdaTestDir = "lambdas\src\test\kotlin\$Group\lambdas"
New-Item -Path "$LambdaTestDir\SumTest.kt" -ItemType "file" -Force
$LambdaMainDir = "lambdas\src\main\kotlin\$Group\lambdas"
New-Item -Path "$LambdaMainDir\Sum.kt" -ItemType "file" -Force
$Group = "com\github\username"
$LambdaTestDir = "lambdas\src\test\kotlin\$Group\lambdas"
ni "$LambdaTestDir\SumTest.kt" -i f -f
$LambdaMainDir = "lambdas\src\main\kotlin\$Group\lambdas"
ni "$LambdaMainDir\Sum.kt" -i f -f
GROUP="com/github/username"
LAMBDA_TEST_DIR="lambdas/src/test/kotlin/$GROUP/lambdas"
mkdir -p "$LAMBDA_TEST_DIR"
touch "$LAMBDA_TEST_DIR/SumTest.kt"
LAMBDA_MAIN_DIR="lambdas/src/main/kotlin/$GROUP/lambdas"
mkdir -p "$LAMBDA_MAIN_DIR"
touch "$LAMBDA_MAIN_DIR/Sum.kt"
Por ejemplo, si quisiéramos definir una función lambda que suma dos números:
- Código esencial
- Código completo
val add: (Int, Int) -> Int = { a, b -> a + b }
add(3, 4) shouldBe 7
package com.github.username.lambdas
val add: (Int, Int) -> Int = { a, b -> a + b }
package com.github.username.lambdas
import io.kotest.core.spec.style.FreeSpec
import io.kotest.matchers.shouldBe
class SumTest : FreeSpec({
"An add function" - {
"can sum two numbers" {
add(3, 4) shouldBe 7
}
}
})
Breve historia y teoría
El concepto de funciones lambda proviene del Cálculo Lambda, una formalización matemática desarrollada por Alonzo Church en la década de 1930. Esta notación sentó las bases para la programación funcional, que influyó en el diseño de muchos lenguajes modernos como Kotlin, Scala, JavaScript y Python.
El Cálculo Lambda introdujo una manera de definir y aplicar funciones de manera concisa, lo que permitió que los lenguajes de programación funcional adoptaran funciones como ciudadanos de primera clase. Esto significa que las funciones pueden ser asignadas a variables, pasadas como argumentos o retornadas desde otras funciones.
Elementos clave del Cálculo Lambda
- Variable (): Un símbolo que representa un parámetro o valor.
- Abstracción (): Una función anónima que toma un parámetro y evalúa una expresión .
- Aplicación (): La aplicación de una función a un argumento . Tanto como pueden ser variables, abstracciones u otras aplicaciones.
Operaciones de reducción
- -conversión: — Cambiar el nombre de un parámetro.
- -reducción: — Sustituir el parámetro de la función por el argumento en su cuerpo.
Kotlin adoptó las funciones lambda para ofrecer una sintaxis concisa y flexible que facilita el uso de funciones de orden superior y la programación funcional, eliminando la necesidad de definir explícitamente funciones con nombre.
El Cálculo Lambda es Turing completo, lo que significa que puede expresar cualquier algoritmo computacional. Este hecho lo convierte en un modelo de computación poderoso y versátil.
Lambdas en Kotlin con funciones de orden superior
Si quieres crear el archivo desde la terminal...
- Windows
- Windows (corto)
- Linux/Mac
New-Item -Path "$LambdaTestDir\ParityTest.kt" -ItemType "file"
ni "$LambdaTestDir\ParityTest.kt" -i f
touch "$LAMBDA_TEST_DIR/ParityTest.kt"
Las funciones lambda son especialmente útiles cuando se combinan con funciones de orden superior.
A continuación, veremos un ejemplo usando filter
, una función de orden superior que toma una lambda como argumento:
- Código esencial
- Código completo
val numbers = listOf(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)
numbers.filter { it % 2 == 0 } shouldBe listOf(2, 4, 6, 8, 10)
package cl.ravenhill.lambdas
import io.kotest.core.spec.style.FreeSpec
import io.kotest.matchers.shouldBe
class ParityTest : FreeSpec({
"A list can be filtered by parity" {
val numbers = listOf(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)
numbers.filter { it % 2 == 0 } shouldBe listOf(2, 4, 6, 8, 10)
}
})
En este caso, la función filter
recibe una lambda que evalúa si un número es par.
En Kotlin, si el último parámetro de una función es una lambda, es buena práctica mover la lambda fuera de los paréntesis. Esto se conoce como trailing lambda y mejora la legibilidad del código. Las siguientes dos llamadas son equivalentes:
val numerosPares = filter(numeros, { it % 2 == 0 })
val numerosPares = filter(numeros) { it % 2 == 0 }
Aplicando lambdas en funciones de orden superior
Si quieres crear el archivo desde la terminal...
- Windows
- Windows (corto)
- Linux/Mac
New-Item -Path "$LambdaTestDir\IntProcessingTest.kt" -ItemType "file"
New-Item -Path "$LambdaMainDir\IntProcessing.kt" -ItemType "file"
ni "$LambdaTestDir\IntProcessingTest.kt" -i f
ni "$LambdaMainDir\IntProcessing.kt" -i f
touch "$LAMBDA_TEST_DIR/IntProcessingTest.kt"
touch "$LAMBDA_MAIN_DIR/IntProcessing.kt"
Puedes crear tus propias funciones de orden superior que acepten lambdas como parámetros. Aquí hay un ejemplo simple:
- Código esencial
- Código completo
fun processInts(a: Int, b: Int, operation: (Int, Int) -> Int) =
operation(a, b)
processInts(3, 4) { a, b -> a + b } shouldBe 7
package com.github.username.lambdas
fun processInts(a: Int, b: Int, operation: (Int, Int) -> Int) =
operation(a, b)
package cl.ravenhill.lambdas
import io.kotest.core.spec.style.FreeSpec
import io.kotest.matchers.shouldBe
class IntProcessingTest : FreeSpec({
"Two integers can be combined with an add operation" {
processInts(3, 4) { a, b -> a + b } shouldBe 7
}
})
- Definimos una función
processInts
que toma dos enteros y una función lambda como argumentos. - La función
processInts
aplica la función lambda a los dos enteros y devuelve el resultado. - En el test, pasamos una lambda que suma los dos enteros
3
y4
a la funciónprocessInts
y comprobamos que el resultado es7
.
Ventajas y desventajas de las funciones lambda
Beneficios
- Concisión y legibilidad: Las funciones lambda permiten escribir código de manera más breve y clara, eliminando la necesidad de declarar y definir funciones completas cuando solo se requiere una lógica simple.
- Flexibilidad: Las lambdas se pueden pasar como argumentos a otras funciones, lo que las hace extremadamente útiles para funciones de orden superior, como
map
,filter
yfold
. - Funcionalidad modular: Facilitan la creación de código modular y reutilizable al encapsular pequeños fragmentos de lógica que pueden ser fácilmente combinados y reutilizados.
- Compatibilidad con la programación funcional: Kotlin adopta elementos de la programación funcional, y las lambdas son un componente central de este paradigma, permitiendo un estilo de programación expresivo y efectivo.
Limitaciones
- Dificultad de depuración: El uso intensivo de lambdas puede hacer que el código sea más difícil de depurar, ya que las funciones anónimas no tienen nombre y los errores pueden no señalar directamente la fuente del problema.
- Complejidad en casos avanzados: Las lambdas son fáciles de usar en casos simples, pero pueden volverse complicadas cuando se manejan tipos complejos o múltiples parámetros, lo que puede afectar la claridad del código.
- Sobrecarga de uso: Aunque son concisas, el uso excesivo de lambdas puede resultar en un código menos legible, especialmente si se anidan lambdas o se utilizan sin contexto suficiente.
- Rendimiento: En algunos casos, las lambdas pueden introducir un pequeño overhead debido a la creación de objetos adicionales en tiempo de ejecución, lo que podría impactar en el rendimiento si no se utilizan con cuidado.
Destructuring declarations
Las declaraciones de desestructuración permiten descomponer objetos complejos en componentes individuales, asignándolos a variables separadas en una sola declaración. Esto simplifica la extracción de valores de estructuras como pares, listas, diccionarios o tipos de datos personalizados, facilitando su manipulación.
Ejemplo
Si quieres crear el archivo desde la terminal...
- Windows
- Windows (corto)
- Linux/Mac
New-Item -Path "$LambdaTestDir\destructuring\PersonTest.kt" `
-ItemType "file" -Force
New-Item -Path "$LambdaMainDir\Person.kt" `
-ItemType "file" -Force
ni "$LambdaTestDir\destructuring\PersonTest.kt" -i f -f
ni "$LambdaMainDir\Person.kt" -i f -f
mkdir -p "$LAMBDA_TEST_DIR/destructuring"
touch "$LAMBDA_TEST_DIR/destructuring/PersonTest.kt"
touch "$LAMBDA_MAIN_DIR/Person.kt"
- Código esencial
- Código completo
data class Person(val name: String, val age: Int)
val person = Person("Alice", 29)
val (name, age) = person
name shouldBe "Alice"
age shouldBe 29
package com.github.username.destructuring
data class Person(val name: String, val age: Int)
package cl.ravenhill.destructuring
import io.kotest.core.spec.style.FreeSpec
import io.kotest.matchers.shouldBe
class PersonTest : FreeSpec({
"A person object" - {
"can be destructured into name and age" {
val person = Person("Alice", 29)
val (name, age) = person
name shouldBe "Alice"
age shouldBe 29
}
}
})
En este ejemplo, descomponemos el objeto person
en sus componentes name
y age
utilizando la sintaxis de desestructuración: val (component1, component2, ...)
. Esta técnica se puede aplicar en diversos escenarios, mejorando la simplicidad y eficiencia en la manipulación de datos.
val (first, second) = listOf(1, 2) // También funciona con listas
first shouldBe 1
second shouldBe 2
Under the Hood
Si quieres crear el archivo desde la terminal...
- Windows
- Windows (corto)
- Linux/Mac
New-Item -Path "$LambdaMainDir\destructuring\Point.kt" `
-ItemType "file" -Force
New-Item -Path "$LambdaTestDir\destructuring\PointTest.kt" `
-ItemType "file" -Force
ni "$LambdaMainDir\destructuring\Point.kt" -i f -f
ni "$LambdaTestDir\destructuring\PointTest.kt" -i f -f
touch "$LAMBDA_MAIN_DIR/destructuring/Point.kt"
touch "$LAMBDA_TEST_DIR/destructuring/PointTest.kt"
En Kotlin, todas las data classes automáticamente declaran operadores componentN
para cada una de las variables de la clase, lo que permite la desestructuración de sus propiedades de manera sencilla. Sin embargo, también es posible personalizar este comportamiento en clases no marcadas como data
, definiendo manualmente los métodos componentN
.
Por ejemplo, podemos desestructurar una clase personalizada como Point
definiendo los operadores correspondientes:
- Código esencial
- Código completo
class Point(val x: Int, val y: Int) {
operator fun component1() = x
operator fun component2() = y
}
val (x, y) = Point(10, 20)
x shouldBe 10
y shouldBe 20
package com.github.username.destructuring
class Point(val x: Int, val y: Int) {
operator fun component1() = x
operator fun component2() = y
}
package cl.ravenhill.destructuring
import io.kotest.core.spec.style.FreeSpec
import io.kotest.matchers.shouldBe
class PointTest : FreeSpec({
"A Point object" - {
"can be destructured into x and y" {
val (x, y) = Point(10, 20)
x shouldBe 10
y shouldBe 20
}
}
})
Este enfoque te permite controlar explícitamente cómo se descomponen las propiedades de la clase, incluso en aquellas que no son data classes
.
Ejercicio: Desestructuración de listas
¿Cuáles son los tipos de head
y tail
?
val (head, tail) = listOf("D.Gray-Man", "Made in Abyss")
val (head, tail) = listOf("D.Gray-Man", "Made in Abyss", "FLCL")
Solución
- En el primer caso,
head
es de tipoString
ytail
es de tipoString
. - En el segundo caso,
head
es de tipoString
ytail
es de tipoList<String>
.
Desestructuración en Lambdas
Si quieres crear el archivo desde la terminal...
- Windows
- Windows (corto)
- Linux/Mac
New-Item -Path "$LambdaMainDir\destructuring\PairProcessing.kt" `
-ItemType "file" -Force
New-Item -Path "$LambdaTestDir\destructuring\PairProcessingTest.kt" `
-ItemType "file" -Force
ni "$LambdaMainDir\destructuring\PairProcessing.kt" -i f -f
ni "$LambdaTestDir\destructuring\PairProcessingTest.kt" -i f -f
touch "$LAMBDA_MAIN_DIR/destructuring/PairProcessing.kt"
touch "$LAMBDA_TEST_DIR/destructuring/PairProcessingTest.kt"
En Kotlin, una lambda puede desestructurar cualquier estructura que tenga definidos sus operadores componentN
. Esto permite acceder fácilmente a los valores de un objeto directamente dentro de la lambda, mejorando la legibilidad y la simplicidad del código.
- Código esencial
- Código completo
val sumPair = { (a, b): Pair<Int, Int> -> a + b }
val increasePairBy = { (a, b): Pair<Int, Int>, n: Int ->
(a + n) to (b + n)
}
sumPair(3 to 4) shouldBe 7
increasePairBy(3 to 4, 2) shouldBe (5 to 6)
package com.github.username.destructuring
val sumPair = { (a, b): Pair<Int, Int> -> a + b }
val increasePairBy = { (a, b): Pair<Int, Int>, n: Int ->
(a + n) to (b + n)
}
package cl.ravenhill.lambdas.destructuring
import io.kotest.core.spec.style.FreeSpec
import io.kotest.matchers.shouldBe
class PairProcessingTest : FreeSpec({
"A pair can be destructured by a lambda" {
sumPair(3 to 4) shouldBe 7
increasePairBy(3 to 4, 2) shouldBe (5 to 6)
}
})
sumPair
desestructura el par(a, b)
y devuelve la suma de sus componentes.increasePairBy
desestructura el par(a, b)
y devuelve un nuevo par donde ambos valores han sido incrementados porn
.
Este uso de desestructuración en lambdas es particularmente útil cuando trabajamos con estructuras de datos más complejas y deseamos acceder a sus valores de manera directa y concisa.
La desestructuración en Kotlin no es compatible con estructuras anidadas, como pares de pares o listas de pares. Para descomponer estructuras anidadas, es necesario realizar la desestructuración de forma manual.
Ejercicio: Desestructuración en lambdas
Indica el número de parámetros que recibe cada una de las siguientes lambdas:
val f1 = { a: Int, b: Int -> TODO() }
val f2 = { (a, b): Pair<Int, Int> -> TODO() }
val f3 = { a: Int, (b, c): Pair<Int, Int> -> TODO() }
val f4 = { (a, b): Pair<Int, Int>, (c, d): Pair<Int, Int> -> TODO() }
Solución
f1
recibe dos parámetros:a
yb
.f2
recibe un parámetro de tipoPair<Int, Int>
.f3
recibe dos parámetros: un enteroa
y un par de enterosb
yc
.f4
recibe dos pares de enteros.
Ventajas y desventajas de la desestructuración
Beneficios
- Simplicidad y legibilidad: Permite descomponer estructuras de datos complejas en elementos individuales de manera clara y concisa, mejorando la legibilidad del código.
- Acceso directo a datos: Facilita el acceso directo a los componentes de un objeto sin necesidad de métodos adicionales, lo que hace que la manipulación de datos sea más eficiente.
- Flexibilidad: La desestructuración se puede aplicar a múltiples tipos de estructuras, como listas, pares, y clases de datos, haciendo que sea una herramienta versátil para manejar datos en diversas situaciones.
- Compatibilidad con lambdas: Se puede usar en funciones lambda, lo que permite escribir expresiones de manera más natural y reducir la necesidad de usar referencias explícitas a las propiedades de un objeto.
Limitaciones
- Dificultad de depuración: En casos complejos o cuando se usa en exceso, puede hacer que el seguimiento de errores sea más difícil, ya que los valores se descomponen directamente y no siempre queda claro de dónde provienen.
- Limitaciones en estructuras anidadas: La desestructuración no soporta estructuras de datos anidadas de forma automática, lo que obliga a realizar la descomposición de manera manual o con múltiples pasos adicionales.
- Potencial de ambigüedad: Cuando se utiliza con estructuras similares, puede ser confuso determinar qué elementos están siendo desestructurados, especialmente si se utiliza en contextos con nombres de variables poco descriptivos.
- Sobrecarga en el rendimiento: En algunos casos, la desestructuración puede introducir una pequeña sobrecarga en tiempo de ejecución, especialmente si se utiliza con frecuencia en bucles o en operaciones críticas de rendimiento.
¿Qué aprendimos?
En esta lección, exploramos en profundidad el concepto de funciones lambda en Kotlin, su definición y uso, así como su relevancia en la programación funcional y en las funciones de orden superior. Repasamos la sintaxis y las ventajas que ofrecen, como la concisión, flexibilidad, y compatibilidad con paradigmas funcionales. También analizamos las declaraciones de desestructuración, destacando su utilidad para simplificar el acceso a los datos y cómo se integran de manera eficiente en lambdas para mejorar la legibilidad y modularidad del código.
Además, abordamos las ventajas y desventajas tanto de las funciones lambda como de la desestructuración, discutiendo cómo su uso adecuado puede mejorar la legibilidad y flexibilidad del código, pero también los desafíos que presentan, como la depuración y limitaciones en casos complejos o anidados.
A partir de este conocimiento, lxs desarrolladorxs pueden aprovechar estas características para escribir código más limpio y eficiente, manteniendo un enfoque en la programación funcional y modular, que es central en Kotlin.
Bibliografías Recomendadas
- 📚 "5. Programming with lambdas". (2017). Dmitry Jemerov, Svetlana Isakova, en Kotlin in action, (1st, pp. 103–132.) Manning Publications Co.