Kotlin

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ícate

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

Aprende Kotlin GRATIS online

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.

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

Kotlin

Introducción Y Entorno

Instalación Y Primer Programa De Kotlin

Kotlin

Introducción Y Entorno

Tipos De Datos, Variables Y Constantes

Kotlin

Sintaxis

Operadores Y Expresiones

Kotlin

Sintaxis

Cadenas De Texto Y Manipulación

Kotlin

Sintaxis

Estructuras De Control

Kotlin

Sintaxis

Funciones Y Llamada De Funciones

Kotlin

Sintaxis

Clases Y Objetos

Kotlin

Programación Orientada A Objetos

Herencia Y Polimorfismo

Kotlin

Programación Orientada A Objetos

Interfaces Y Clases Abstractas

Kotlin

Programación Orientada A Objetos

Data Classes Y Destructuring

Kotlin

Programación Orientada A Objetos

Tipos Genéricos Y Varianza

Kotlin

Programación Orientada A Objetos

Listas, Conjuntos Y Mapas

Kotlin

Estructuras De Datos

Introducción A La Programación Funcional

Kotlin

Programación Funcional

Funciones De Primera Clase Y De Orden Superior

Kotlin

Programación Funcional

Inmutabilidad Y Datos Inmutables

Kotlin

Programación Funcional

Composición De Funciones

Kotlin

Programación Funcional

Monads Y Manejo Funcional De Errores

Kotlin

Programación Funcional

Operaciones Funcionales En Colecciones

Kotlin

Programación Funcional

Transformaciones En Monads Y Functors

Kotlin

Programación Funcional

Funciones Parciales Y Currificación

Kotlin

Programación Funcional

Introducción A Las Corutinas

Kotlin

Coroutines Y Asincronía

Asincronía Con Suspend, Async Y Await

Kotlin

Coroutines Y Asincronía

Concurrencia Funcional

Kotlin

Coroutines Y Asincronía

Evaluación

Kotlin

Evaluación

Accede GRATIS a Kotlin y certifícate

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

  1. Comprender el concepto de funciones de primera clase y su aplicación en Kotlin.
  2. Utilizar funciones de orden superior para recibir y devolver otras funciones.
  3. Implementar expresiones lambda y entender su sintaxis.
  4. Diferenciar entre funciones anónimas y expresiones lambda.
  5. Aplicar el uso de funciones inline, no-inline y crossinline para optimización.
  6. Manipular colecciones con funciones de orden superior y lambdas.
  7. Entender el concepto de clausura en las lambdas de Kotlin.