Kotlin: Programación funcional

Kotlin: descubre cómo aplicar la programación funcional en el desarrollo moderno. Aprende conceptos clave y prácticas recomendadas para mejorar tu código de forma eficiente.

Aprende Kotlin GRATIS y certifícate

La programación funcional es un paradigma que trata el cálculo como la evaluación de funciones matemáticas y evita el cambio de estado y los datos mutables. Kotlin, como lenguaje moderno y versátil, ofrece un sólido soporte para este estilo de programación, permitiendo combinarlo con la programación orientada a objetos de manera fluida.

A continuación, exploraremos cómo Kotlin implementa los conceptos fundamentales de la programación funcional y cómo puedes aprovecharlos para escribir código más conciso, legible y fácil de mantener.

Funciones de orden superior

En Kotlin, las funciones son ciudadanos de primera clase. Esto significa que puedes asignarlas a variables, pasarlas como parámetros y retornarlas desde otras funciones. Las funciones que toman otras funciones como parámetros o las devuelven se denominan funciones de orden superior.

Ejemplo:

fun <T> filtrar(lista: List<T>, criterio: (T) -> Boolean): List<T> {
    val resultado = mutableListOf<T>()
    for (elemento in lista) {
        if (criterio(elemento)) {
            resultado.add(elemento)
        }
    }
    return resultado
}

// Uso de la función
val numeros = listOf(1, 2, 3, 4, 5)
val pares = filtrar(numeros) { it % 2 == 0 }
println(pares) // Imprime: [2, 4]

En este ejemplo, filtrar es una función genérica que acepta una lista y un criterio de filtrado. El criterio es una función que recibe un elemento y devuelve un booleano.

Lambdas y funciones anónimas

Las expresiones lambda son funciones anónimas que se pueden utilizar cuando se necesita una función como argumento. En Kotlin, las lambdas se representan con la sintaxis { parámetros -> cuerpo }.

Ejemplo:

val suma = { a: Int, b: Int -> a + b }
println(suma(3, 4)) // Imprime: 7

Si el tipo puede inferirse, es posible omitirlo:

val numeros = listOf(1, 2, 3, 4, 5)
val cuadrados = numeros.map { it * it }
println(cuadrados) // Imprime: [1, 4, 9, 16, 25]

Funciones puras y transparencia referencial

Una función pura es aquella que siempre devuelve el mismo resultado para los mismos argumentos y no produce efectos secundarios. Esto significa que no modifica variables externas ni interactúa con el mundo exterior.

Ejemplo de función pura:

fun multiplicar(a: Int, b: Int): Int {
    return a * b
}

Ejemplo de función con efectos secundarios:

var contador = 0

fun incrementarContador() {
    contador += 1
}

Las funciones puras favorecen la transparencia referencial, lo que facilita la razón acerca del código y su prueba unitaria.

Composición de funciones

La composición de funciones permite combinar funciones simples para crear funciones más complejas. En Kotlin, aunque no existe un operador de composición nativo, es posible implementar esta funcionalidad.

Implementación de composición:

infix fun <A, B, C> ((A) -> B).componer(otra: (B) -> C): (A) -> C {
    return { a: A -> otra(this(a)) }
}

Uso de la composición:

val duplicar = { x: Int -> x * 2 }
val incrementar = { x: Int -> x + 1 }

val duplicarEIncrementar = duplicar componer incrementar
println(duplicarEIncrementar(3)) // Imprime: 7

Aquí, duplicarEIncrementar es el resultado de componer duplicar seguido de incrementar.

Expresiones lambda con receptor

Las lambdas con receptor permiten acceder a los miembros del receptor dentro del cuerpo de la lambda sin necesidad de referencias explícitas. Se definen con la sintaxis Type.(...) -> ReturnType.

Ejemplo:

val construirPersona: Persona.() -> Unit = {
    nombre = "Juan"
    edad = 30
}

class Persona {
    var nombre: String = ""
    var edad: Int = 0
}

val persona = Persona().apply(construirPersona)
println(persona.nombre) // Imprime: Juan

Las lambdas con receptor son especialmente útiles en la creación de DSLs (Domain Specific Languages).

Funciones de ámbito

Las funciones de ámbito en Kotlin (let, run, with, apply, also) permiten ejecutar un bloque de código en el contexto de un objeto.

  • let: devuelve el resultado de la lambda.
  • run: similar a let, pero usa this en lugar de it.
  • with: una función que toma un receptor y una lambda, y devuelve el resultado de la lambda.
  • apply: devuelve el objeto original después de aplicar la lambda.
  • also: devuelve el objeto original y usa it como receptor.

Ejemplo con also:

val lista = mutableListOf(1, 2, 3).also {
    println("Lista inicial: $it")
    it.add(4)
}
println("Lista modificada: $lista")
// Imprime:
// Lista inicial: [1, 2, 3]
// Lista modificada: [1, 2, 3, 4]

Inmutabilidad y datos inmutables

La inmutabilidad es clave en la programación funcional. En Kotlin, se promueve el uso de variables inmutables y estructuras de datos inmutables.

Declaración de variables inmutables:

val constante = 42
// constante = 24 // Error: no se puede reasignar una val

Uso de datos inmutables:

data class Punto(val x: Int, val y: Int)

val punto = Punto(1, 2)
// punto.x = 3 // Error: no se puede reasignar una val

Los data class con propiedades val crean objetos inmutables, lo que previene modificaciones accidentales.

Manejo funcional de errores con Result

El tipo Result en Kotlin permite manejar operaciones que pueden fallar de forma funcional, encapsulando el éxito o el error.

Ejemplo:

fun parsearEntero(cadena: String): Result<Int> {
    return try {
        Result.success(cadena.toInt())
    } catch (e: NumberFormatException) {
        Result.failure(e)
    }
}

val resultado = parsearEntero("123")
resultado.fold(
    onSuccess = { println("Número: $it") },
    onFailure = { println("Error: ${it.message}") }
)
// Imprime: Número: 123

Utilizando Result, se evita el uso excesivo de excepciones y se promueve un flujo de control más funcional.

Corutinas y programación asíncrona

Las corutinas son una característica potente en Kotlin para manejar la concurrencia de forma sencilla y eficiente, evitando los callbacks y el código complejo.

Ejemplo con async y await:

import kotlinx.coroutines.*

suspend fun tareaLargaDuracion(): Int {
    delay(1000L)
    return 42
}

fun main() = runBlocking {
    val resultado = async { tareaLargaDuracion() }
    println("Calculando...")
    println("Resultado: ${resultado.await()}")
}
// Imprime:
// Calculando...
// Resultado: 42

Aquí, tareaLargaDuracion es una función que simula una operación costosa. Con async, se inicia de forma asíncrona y await se utiliza para obtener el resultado.

Operaciones funcionales en colecciones

Kotlin proporciona un rico conjunto de funciones para operar sobre colecciones de manera funcional.

Filtro y transformación:

val nombres = listOf("Ana", "Juan", "Pedro", "María")
val nombresLargos = nombres.filter { it.length > 3 }.map { it.uppercase() }
println(nombresLargos) // Imprime: [JUAN, PEDRO, MARÍA]
  • filter: selecciona elementos que cumplen una condición.
  • map: transforma cada elemento según una función.

Reducción y acumulación:

val numeros = listOf(1, 2, 3, 4, 5)
val suma = numeros.reduce { acc, numero -> acc + numero }
println(suma) // Imprime: 15

val producto = numeros.fold(1) { acc, numero -> acc * numero }
println(producto) // Imprime: 120
  • reduce: combina los elementos de una colección utilizando una función acumulativa.
  • fold: similar a reduce, pero permite especificar un valor inicial.

Secuencias y procesamiento perezoso

Las secuencias (Sequence) permiten procesar colecciones de manera perezosa, lo que es eficiente cuando se trabaja con datos grandes o infinitos.

Ejemplo:

val numerosInfinitos = generateSequence(1) { it + 1 }
val sumaDiezPrimeros = numerosInfinitos.take(10).sum()
println(sumaDiezPrimeros) // Imprime: 55

En este caso, generateSequence crea una secuencia infinita, pero gracias a take(10), solo procesamos los primeros 10 elementos.

Patrón de inmutabilidad con copiado de datos

Al trabajar con objetos inmutables, es común crear nuevas instancias con algunas modificaciones. Los data class facilitan este proceso con la función copy.

Ejemplo:

data class Usuario(val nombre: String, val edad: Int)

val usuario1 = Usuario("Laura", 28)
val usuario2 = usuario1.copy(edad = 29)
println(usuario1) // Imprime: Usuario(nombre=Laura, edad=28)
println(usuario2) // Imprime: Usuario(nombre=Laura, edad=29)

Al utilizar copy, se crea un nuevo objeto con los cambios especificados, manteniendo la inmutabilidad.


Con estas características y prácticas, Kotlin ofrece un entorno propicio para aprovechar los beneficios de la programación funcional, combinando claridad, concisión y eficiencia en el desarrollo de aplicaciones modernas.

Empezar curso de Kotlin

Lecciones de este módulo de Kotlin

Lecciones de programación del módulo Programación funcional del curso de Kotlin.

Ejercicios de programación en este módulo de Kotlin

Evalúa tus conocimientos en Programación funcional con ejercicios de programación Programación funcional de tipo Test, Puzzle, Código y Proyecto con VSCode.