Kotlin
Tutorial Kotlin: Funciones de primera clase y de orden superior
Entra en el mundo de la programación funcional con Kotlin. Aprende sobre funciones de primera clase y de orden superior, lambdas y optimización con inline.
Aprende Kotlin GRATIS y certifícateFunciones de primera clase y de orden superior
En Kotlin, las funciones son ciudadanos de primera clase, lo que significa que se pueden tratar como cualquier otro valor. Pueden asignarse a variables, almacenarse en estructuras de datos, pasarse como argumentos a otras funciones y retornar desde funciones.
Por ejemplo, asignar una función a una variable:
val saludo = { nombre: String -> "Hola, $nombre" }
println(saludo("María")) // Imprime: Hola, María
Las funciones de orden superior son funciones que reciben otras funciones como parámetros o que retornan una función. Esto permite crear abstracciones más flexibles y reutilizables.
Un ejemplo de función de orden superior que recibe una función como parámetro:
fun operar(a: Int, b: Int, operacion: (Int, Int) -> Int): Int {
return operacion(a, b)
}
val suma = operar(5, 3) { x, y -> x + y }
println(suma) // Imprime: 8
En este caso, operar
recibe dos enteros y una función operacion
que define la operación a realizar. Al llamar a operar
, proporcionamos una expresión lambda que suma los dos números.
También es posible crear funciones que retornan otras funciones:
fun crearMultiplicador(factor: Int): (Int) -> Int {
return { numero -> numero * factor }
}
val duplicar = crearMultiplicador(2)
println(duplicar(4)) // Imprime: 8
Aquí, crearMultiplicador
devuelve una función que multiplica un número por el factor
dado. La variable duplicar
es una función que duplica cualquier número que se le pase.
El uso de funciones de primera clase y de orden superior permite escribir código más modular y conciso. Facilita la composición de comportamientos y promueve la reutilización de código al separar la lógica en funciones pequeñas y especializadas.
Expresiones Lambda
Las expresiones lambda en Kotlin son funciones anónimas que pueden ser tratadas como valores. Permiten definir funciones de manera concisa sin necesidad de asignarles un nombre explícito, lo que facilita la programación funcional y el uso de funciones de orden superior.
La sintaxis básica de una expresión lambda es:
{ parámetros -> cuerpo }
Por ejemplo, una expresión lambda que suma dos números enteros se puede definir así:
val sumar = { a: Int, b: Int -> a + b }
println(sumar(2, 3)) // Imprime: 5
En este caso, sumar
es una variable que contiene una función anónima que suma sus dos argumentos.
Cuando una expresión lambda tiene un único parámetro, se puede utilizar la palabra clave it
para referirse a dicho parámetro sin nombrarlo explícitamente. Esto simplifica aún más la sintaxis:
val números = listOf(1, 2, 3, 4, 5)
val cuadrados = números.map { it * it }
println(cuadrados) // Imprime: [1, 4, 9, 16, 25]
Aquí, la función map
aplica la expresión lambda a cada elemento de la lista, y it
representa cada número en iteración.
Las expresiones lambda pueden acceder a variables del contexto en el que fueron definidas. Este comportamiento se conoce como clausura y permite que la lambda capture y utilice variables externas:
var factor = 10
val multiplicarPorFactor = { número: Int -> número * factor }
println(multiplicarPorFactor(5)) // Imprime: 50
factor = 3
println(multiplicarPorFactor(5)) // Imprime: 15
En este ejemplo, la lambda multiplicarPorFactor
utiliza la variable externa factor
, y refleja los cambios que se hagan en ella.
Si el tipo de los parámetros y el de retorno pueden ser inferidos por el compilador, es posible omitirlos para hacer el código más conciso:
val saludar = { nombre: String -> println("Hola, $nombre") }
saludar("Carlos") // Imprime: Hola, Carlos
Sin embargo, si se desea mayor claridad o si el compilador no puede inferir los tipos, es recomendable especificarlos.
Las lambdas también pueden contener múltiples expresiones. Si la última expresión de la lambda es una expresión, su resultado será el valor retornado:
val calcularEdad = { añoNacimiento: Int ->
val añoActual = 2024
añoActual - añoNacimiento
}
println(calcularEdad(1990)) // Imprime: 34
En casos donde la lógica es más compleja, se puede utilizar el retorno explícito para mejorar la legibilidad:
val esPar = { número: Int ->
if (número % 2 == 0) {
true
} else {
false
}
}
println(esPar(4)) // Imprime: true
Las expresiones lambda son fundamentales en Kotlin para trabajar con colecciones y flujos de datos, permitiendo manipular y transformar datos de forma elegante. Su uso promueve un estilo de programación funcional y favorece la creación de código más legible y mantenible.
Lambdas con funciones de orden superior
En Kotlin, las funciones de orden superior son fundamentales para aprovechar al máximo las lambdas. Una función de orden superior es aquella que recibe una función como parámetro o devuelve otra función. Las lambdas son ideales para pasar comportamientos a estas funciones, permitiendo una programación más flexible y concisa.
Por ejemplo, la función map
de las colecciones aplica una transformación a cada elemento:
val números = listOf(1, 2, 3)
val cuadrados = números.map { número -> número * número }
println(cuadrados) // Imprime: [1, 4, 9]
En este caso, la lambda { número -> número * número }
se pasa a map
para definir cómo transformar cada elemento.
Cuando la lambda tiene un solo parámetro, Kotlin permite utilizar it
como nombre implícito del parámetro, haciendo el código más sintético:
val cubos = números.map { it * it * it }
println(cubos) // Imprime: [1, 8, 27]
Las lambdas también pueden utilizarse con funciones propias. Imaginemos una función de orden superior que filtra elementos basándose en un criterio proporcionado:
fun filtrarNúmeros(números: List<Int>, criterio: (Int) -> Boolean): List<Int> {
val resultado = mutableListOf<Int>()
for (número in números) {
if (criterio(número)) {
resultado.add(número)
}
}
return resultado
}
val númerosPares = filtrarNúmeros(números) { it % 2 == 0 }
println(númerosPares) // Imprime: [2]
Aquí, la lambda { it % 2 == 0 }
define el criterio para filtrar números pares, demostrando cómo las funciones de orden superior pueden recibir comportamientos como argumentos.
Es posible utilizar lambdas con múltiples parámetros en funciones de orden superior. Por ejemplo, una función que opera sobre dos números:
fun operar(a: Int, b: Int, operación: (Int, Int) -> Int): Int {
return operación(a, b)
}
val suma = operar(4, 5) { x, y -> x + y }
println("Suma: $suma") // Imprime: Suma: 9
val producto = operar(4, 5) { x, y -> x * y }
println("Producto: $producto") // Imprime: Producto: 20
Las lambdas permiten pasar diferentes operaciones sin tener que definir funciones adicionales, lo cual es práctico y versátil.
En cuanto a la sintaxis, cuando una función de orden superior tiene la lambda como último parámetro, podemos utilizar la notación de lambda al final y colocarla fuera de los paréntesis:
números.forEach { println(it) }
Si la función solo recibe una lambda como argumento, los paréntesis pueden omitirse completamente:
run {
println("Ejecutando una lambda con run")
}
Al trabajar con funciones de orden superior y lambdas, es común utilizar funciones de la biblioteca estándar de Kotlin, como filter
, map
, reduce
, entre otras. Estas funciones facilitan la manipulación de colecciones de forma funcional y elegante.
Por ejemplo, calcular el factorial de una lista de números:
val factoriales = números.map { número ->
(1..número).reduce { acc, i -> acc * i }
}
println(factoriales) // Imprime: [1, 2, 6]
En este ejemplo, se utiliza una lambda más compleja dentro de map
, que a su vez emplea reduce
para calcular el factorial de cada número.
Es importante tener en cuenta los tipos de función al utilizar lambdas con funciones de orden superior. Las lambdas deben coincidir con el tipo esperado por la función:
fun procesarTexto(texto: String, acción: (String) -> Unit) {
acción(texto)
}
procesarTexto("Kotlin es genial") { println(it.uppercase()) }
// Imprime: KOTLIN ES GENIAL
Si la lambda no coincide con el tipo requerido, el compilador generará un error, por lo que es esencial asegurarse de que los tipos son correctos.
Finalmente, las funciones de orden superior con lambdas son una herramienta potente para crear código reutilizable y modular. Facilitan la implementación de patrones funcionales y permiten escribir código más declarativo.
Por ejemplo, podríamos definir una función genérica para medir el tiempo de ejecución de cualquier bloque de código:
fun medirTiempo(acción: () -> Unit) {
val inicio = System.currentTimeMillis()
acción()
val fin = System.currentTimeMillis()
println("Tiempo transcurrido: ${fin - inicio} ms")
}
medirTiempo {
for (i in 1..1_000_000) { /* Operación intensiva */ }
}
Esta función de orden superior acepta una lambda sin parámetros y puede utilizarse para envolver cualquier acción cuyo tiempo de ejecución queramos medir.
En resumen, el uso de lambdas con funciones de orden superior en Kotlin permite escribir código más expresivo y conciso, aprovechando al máximo las capacidades del lenguaje para la programación funcional.
Cláusulas inline y no-inline
En Kotlin, las funciones inline son una característica que permite optimizar el rendimiento al trabajar con funciones de orden superior. Al marcar una función como inline
, el compilador sustituye las llamadas a dicha función insertando su código directamente en los lugares donde se invoca. Esto reduce la sobrecarga asociada a la creación de objetos para las funciones lambda y evita llamadas adicionales en tiempo de ejecución.
Por ejemplo, consideremos una función de orden superior que aplica una operación a un entero:
fun aplicarOperacion(numero: Int, operacion: (Int) -> Int): Int {
return operacion(numero)
}
Al utilizarla, se crea un objeto lambda para la función proporcionada, lo cual implica un coste de rendimiento. Si marcamos la función como inline
, este coste se elimina:
inline fun aplicarOperacion(numero: Int, operacion: (Int) -> Int): Int {
return operacion(numero)
}
Ahora, el cuerpo de aplicarOperacion
y la lambda pasada se insertan en el punto de llamada, mejorando la eficiencia del código generado.
Sin embargo, en ocasiones no deseamos aplicar el inline a todas las funciones lambda que se pasan como parámetros. Para estos casos, Kotlin ofrece la palabra clave noinline
. Al marcar un parámetro de tipo función como noinline
, indicamos que esa lambda no debe ser inlined.
Por ejemplo:
inline fun transformarTexto(
texto: String,
noinline formato: (String) -> String,
procesamiento: (String) -> String
): String {
val textoFormateado = formato(texto)
return procesamiento(textoFormateado)
}
En este caso, la función formato
no será inlined, mientras que procesamiento
sí lo será. Esto es útil si necesitamos pasar la lambda formato
como argumento a otra función o almacenarla en una variable, manteniendo su referencia.
Otra consideración importante al usar funciones inline es el comportamiento de los retornos dentro de las lambdas. Por defecto, las lambdas inlined permiten realizar retornos no locales, es decir, podemos usar return
dentro de la lambda para salir de la función que la envuelve. Si queremos evitar este comportamiento, podemos usar la palabra clave crossinline
.
Por ejemplo:
inline fun recorrerLista(lista: List<Int>, crossinline accion: (Int) -> Unit) {
for (elemento in lista) {
accion(elemento)
}
}
Al marcar accion
con crossinline
, impedimos que la lambda pasada pueda realizar retornos no locales. Cualquier intento de usar return
dentro de accion
será local a la lambda, lo que garantiza un flujo de control predecible.
Las funciones inline también permiten el uso de tipos genéricos reified, lo cual es imposible en funciones normales debido al borrado de tipos en tiempo de ejecución. Al marcar un parámetro de tipo genérico con reified
dentro de una función inline, podemos acceder a la información del tipo en tiempo de ejecución.
Ejemplo:
inline fun <reified T> imprimirTipo() {
println("El tipo es ${T::class.simpleName}")
}
imprimirTipo<String>() // Imprime: El tipo es String
Gracias a reified
, podemos obtener la clase de T
y utilizarla dentro de la función, lo que añade flexibilidad al manejo de tipos genéricos.
Es crucial tener en cuenta que el uso excesivo de funciones inline puede aumentar el tamaño del bytecode generado, ya que se inserta el código de la función en cada punto de invocación. Por ello, es recomendable usar inline
solo cuando los beneficios en rendimiento justifican su uso, especialmente en funciones pequeñas y llamadas frecuentemente.
Asimismo, las funciones inline tienen limitaciones. No se puede aplicar inline
a funciones que no sean de orden superior, es decir, que no reciban parámetros de tipo función. Además, no es posible tener funciones locales o variables locales dentro de una función inline que sean marcadas como inline
o noinline
.
Al combinar inline
con noinline
y crossinline
, podemos controlar con precisión cómo se comportan las funciones lambda pasadas como parámetros. Esto nos permite escribir código que es tanto eficiente como seguro, evitando comportamientos inesperados debido a retornos no locales o captura indebida de variables.
Por ejemplo, si deseamos medir el tiempo que tarda en ejecutarse una operación sin incurrir en sobrecargas innecesarias:
inline fun medirTiempo(operacion: () -> Unit) {
val inicio = System.currentTimeMillis()
operacion()
val fin = System.currentTimeMillis()
println("Tiempo transcurrido: ${fin - inicio} ms")
}
medirTiempo {
// Código cuya ejecución queremos medir
}
Al ser inline, la llamada a medirTiempo
no genera llamadas adicionales en tiempo de ejecución, y la lambda pasada no implica la creación de objetos extra, lo que resulta en un código más rápido.
En conclusión, las cláusulas inline
, noinline
y crossinline
en Kotlin son herramientas poderosas que nos permiten optimizar el rendimiento y controlar el comportamiento de las funciones de orden superior y las lambdas. Al entender y utilizar estas características correctamente, podemos escribir código más eficiente sin comprometer la legibilidad ni la modularidad del programa.
Uso de funciones anónimas
En Kotlin, las funciones anónimas son una forma de definir funciones sin asignarles un nombre específico, utilizando la palabra clave fun
. A diferencia de las expresiones lambda, las funciones anónimas permiten especificar explícitamente los tipos de los parámetros y el tipo de retorno, lo que puede ser útil en ciertas situaciones.
La sintaxis básica de una función anónima es la siguiente:
fun(parámetros): TipoRetorno {
// Cuerpo de la función
}
Por ejemplo, se puede crear una función anónima que calcula la suma de dos números enteros:
val sumar = fun(a: Int, b: Int): Int {
return a + b
}
println(sumar(5, 7)) // Imprime: 12
En este caso, sumar
es una variable que contiene una función anónima que suma dos enteros. Las funciones anónimas son especialmente útiles cuando se necesita especificar el tipo de retorno o cuando se requiere mayor claridad en la declaración de los tipos.
Un aspecto importante de las funciones anónimas es su comportamiento respecto al uso de return
. En una expresión lambda, return
representa un retorno no local, es decir, retorna de la función que la contiene. En cambio, en una función anónima, return
es local a la función anónima. Esto permite controlar de manera más precisa el flujo de ejecución.
Por ejemplo, al recorrer una lista y utilizar una función anónima:
val números = listOf(1, 2, 3, 4, 5)
números.forEach(fun(número) {
if (número == 3) return
println(número)
})
// Imprime:
// 1
// 2
// 4
// 5
En este ejemplo, cuando el número es 3, return
termina únicamente la ejecución de la función anónima para ese elemento, pero el bucle continúa con los siguientes números. Esto difiere del comportamiento que se obtendría con una expresión lambda.
Las funciones anónimas también son útiles cuando se desea utilizar recursión. A diferencia de las lambdas, las funciones anónimas pueden llamarse a sí mismas dentro de su propio cuerpo, ya que pueden referirse a sí mismas de manera directa.
Ejemplo de una función anónima recursiva que calcula el factorial de un número:
val factorial = fun(n: Int): Int {
return if (n == 0) 1 else n * factorial(n - 1)
}
println(factorial(5)) // Imprime: 120
Aquí, la función anónima factorial
se llama a sí misma, permitiendo calcular el factorial de forma recursiva. Este enfoque no es posible con las expresiones lambda, ya que no pueden referenciarse a sí mismas directamente.
Las funciones anónimas pueden acceder a las variables del ámbito externo, gracias a las cláusulas de cierre. Esto permite crear funciones que mantienen un estado o que interactúan con variables definidas fuera de su propio ámbito.
Ejemplo:
var contador = 0
val incrementar = fun() {
contador++
}
incrementar()
incrementar()
println(contador) // Imprime: 2
Es posible utilizar funciones anónimas como argumentos en funciones de orden superior. Por ejemplo, al utilizar la función filter
para filtrar elementos de una lista:
val númerosPares = números.filter(fun(número): Boolean {
return número % 2 == 0
})
println(númerosPares) // Imprime: [2, 4]
En este caso, la función anónima define el criterio de filtrado de manera explícita, lo que puede mejorar la legibilidad en situaciones complejas.
Cuando se trabaja con funciones inline, las funciones anónimas son útiles para controlar los retornos. Dado que las lambdas en funciones inline permiten retornos no locales, puede ser preferible usar una función anónima para evitar salidas inesperadas de la función envolvente.
Ejemplo con una función inline:
inline fun procesar(numeros: List<Int>, acción: (Int) -> Unit) {
numeros.forEach {
acción(it)
}
}
procesar(numeros, fun(numero) {
if (numero == 3) return
println(numero)
})
// Imprime:
// 1
// 2
// 4
// 5
Aquí, el return
dentro de la función anónima es local, lo que mantiene el flujo de ejecución dentro de procesar
.
Además, las funciones anónimas pueden mejorar la legibilidad cuando se trabaja con lógica compleja que podría resultar confusa en una expresión lambda más concisa.
Por ejemplo:
val calcularImpuesto = fun(salario: Double): Double {
val umbral = 30000.0
val tasa = if (salario > umbral) 0.2 else 0.1
return salario * tasa
}
println(calcularImpuesto(25000.0)) // Imprime: 2500.0
println(calcularImpuesto(40000.0)) // Imprime: 8000.0
En este caso, la función anónima facilita la comprensión de los cálculos internos, gracias a su estructura más explícita.
En conclusión, las funciones anónimas en Kotlin ofrecen una alternativa valiosa a las expresiones lambda cuando se necesita mayor control sobre los tipos, el comportamiento de retorno o se requiere una estructura más clara para lógica compleja. Su uso adecuado puede contribuir a escribir código más robusto y fácil de mantener.
Ejercicios de esta lección Funciones de primera clase y de orden superior
Evalúa tus conocimientos de esta lección Funciones de primera clase y de orden superior con nuestros retos de programación de tipo Test, Puzzle, Código y Proyecto con VSCode, guiados por IA.
Clases genéricas con varianza y restricciones
Introducción a las corutinas
Uso de asincronía con suspend, async y await
Formateo de cadenas texto
Uso de monads y manejo funcional de errores
Declaración y uso de variables y constantes
Uso de la concurrencia funcional con corutinas
Operaciones en colecciones
Uso de clases y objetos en Kotlin
Evaluación Kotlin
Funciones de orden superior y expresiones lambda en Kotlin
Herencia y polimorfismo en Kotlin
Inmutabilidad y datos inmutables
Uso de funciones parciales y currificaciones
Primer programa en Kotlin
Introducción a la programación funcional
Introducción a Kotlin
Uso de operadores y expresiones
Sistema de inventario de tienda
Uso de data classes y destructuring
Composición de funciones
Uso de interfaces y clases abstractas
Simulador de conversión de monedas
Programación funcional y concurrencia
Creación y uso de listas, conjuntos y mapas
Transformación en monads y functors
Crear e invocar funciones
Uso de las estructuras de control
Todas las lecciones de Kotlin
Accede a todas las lecciones de Kotlin y aprende con ejemplos prácticos de código y ejercicios de programación con IDE web sin instalar nada.
Introducción A Kotlin
Introducción Y Entorno
Instalación Y Primer Programa De Kotlin
Introducción Y Entorno
Tipos De Datos, Variables Y Constantes
Sintaxis
Operadores Y Expresiones
Sintaxis
Cadenas De Texto Y Manipulación
Sintaxis
Estructuras De Control
Sintaxis
Funciones Y Llamada De Funciones
Sintaxis
Clases Y Objetos
Programación Orientada A Objetos
Herencia Y Polimorfismo
Programación Orientada A Objetos
Interfaces Y Clases Abstractas
Programación Orientada A Objetos
Data Classes Y Destructuring
Programación Orientada A Objetos
Tipos Genéricos Y Varianza
Programación Orientada A Objetos
Listas, Conjuntos Y Mapas
Estructuras De Datos
Introducción A La Programación Funcional
Programación Funcional
Funciones De Primera Clase Y De Orden Superior
Programación Funcional
Inmutabilidad Y Datos Inmutables
Programación Funcional
Composición De Funciones
Programación Funcional
Monads Y Manejo Funcional De Errores
Programación Funcional
Operaciones Funcionales En Colecciones
Programación Funcional
Transformaciones En Monads Y Functors
Programación Funcional
Funciones Parciales Y Currificación
Programación Funcional
Introducción A Las Corutinas
Coroutines Y Asincronía
Asincronía Con Suspend, Async Y Await
Coroutines Y Asincronía
Concurrencia Funcional
Coroutines Y Asincronía
Evaluación
Evaluación
Certificados de superación de Kotlin
Supera todos los ejercicios de programación del curso de Kotlin y obtén certificados de superación para mejorar tu currículum y tu empleabilidad.
En esta lección
Objetivos de aprendizaje de esta lección
- Comprender el concepto de funciones de primera clase y su aplicación en Kotlin.
- Utilizar funciones de orden superior para recibir y devolver otras funciones.
- Implementar expresiones lambda y entender su sintaxis.
- Diferenciar entre funciones anónimas y expresiones lambda.
- Aplicar el uso de funciones inline, no-inline y crossinline para optimización.
- Manipular colecciones con funciones de orden superior y lambdas.
- Entender el concepto de clausura en las lambdas de Kotlin.