Universidad del Quindío
Programa de Ingeniería de Sistemas y Computación
Título: Programación funcional en Kotlin
Docente: Carlos Andrés Florez V.
Existen varios paradigmas de programación, cada uno con sus propias filosofías y enfoques para resolver problemas. Podemos clasificar los paradigmas de programación en dos grandes categorías: imperativos y declarativos.
Los paradigmas imperativos, como la programación estructurada y la programación orientada a objetos, se centran en describir cómo se debe realizar una tarea mediante una serie de instrucciones que modifican el estado del programa. En contraste, los paradigmas declarativos, como la programación funcional y la programación lógica, se enfocan en describir qué se quiere lograr sin especificar los pasos exactos para hacerlo.
La programación de aplicaciones móviles modernas usan el paradigma declarativo y funcional para mejorar la legibilidad, mantenibilidad y escalabilidad del código. Kotlin, el lenguaje oficial para el desarrollo de Android, soporta plenamente la programación funcional, permitiendo a los desarrolladores escribir código más limpio y eficiente.
La programación funcional se basa en varios pilares fundamentales que la diferencian de otros paradigmas como la programación imperativa u orientada a objetos. A continuación, se describen estos pilares con ejemplos prácticos en Kotlin.
Los datos no cambian una vez que se crean. En lugar de modificar estructuras existentes, se crean nuevas versiones con los cambios aplicados. Esto reduce errores relacionados con efectos secundarios y hace el código más predecible.
// Inmutable - usando val
val lista = listOf(1, 2, 3)
val nuevaLista = lista + 4 // Crea una nueva lista
// Mutable
var listaMutable = mutableListOf(1, 2, 3)
listaMutable.add(4) // Modifica la lista original
data class Usuario(val nombre: String, val edad: Int)
val usuario = Usuario("Juan", 25)
val usuarioActualizado = usuario.copy(edad = 26) // Nueva instancia
Una función es pura si:
Esto facilita la depuración y las pruebas unitarias.
fun sumar(a: Int, b: Int): Int = a + b // Siempre devuelve lo mismo
fun calcularArea(radio: Double): Double = Math.PI * radio * radio
var contador = 0
fun incrementarContador(): Int {
contador++ // Efecto secundario
return contador
}
El ejemplo anterior muestra una función que modifica una variable externa, lo que la hace impura, a esta característica se le conoce como efecto secundario.
Un efecto secundario es cualquier cambio de estado que ocurre fuera del ámbito de una función. Esto incluye modificar variables globales, interactuar con bases de datos, escribir en archivos o imprimir en la consola.
Ejemplos de efectos secundarios:
Un lenguaje funcional busca minimizar o eliminar los efectos secundarios para mejorar la predictibilidad y la facilidad de razonamiento sobre el código.
Similar a las lambdas de Java 8+, pero con una sintaxis más concisa. La diferencia principal es que en Kotlin las lambdas van entre llaves {}, usan -> para separar parámetros del cuerpo, y cuando hay un solo parámetro se puede usar la referencia implícita it:
val lambda = { x: Int, y: Int -> x + y }
val resultado = lambda(5, 3) // 8
// Lambda con un solo parámetro (it implícito)
val numeros = listOf(1, 2, 3, 4, 5)
val pares = numeros.filter { it % 2 == 0 }
Las funciones pueden recibir otras funciones como parámetros o devolver funciones como resultado. Esto permite crear abstracciones más poderosas y reutilizables.
fun aplicarOperacion(a: Int, b: Int, operacion: (Int, Int) -> Int): Int {
return operacion(a, b)
}
val suma = aplicarOperacion(5, 3) { x, y -> x + y }
val multiplicacion = aplicarOperacion(5, 3) { x, y -> x * y }
fun crearMultiplicador(factor: Int): (Int) -> Int {
return { numero -> numero * factor }
}
val duplicar = crearMultiplicador(2)
val triplicar = crearMultiplicador(3)
println(duplicar(5)) // 10
println(triplicar(5)) // 15
Los valores y expresiones solo se calculan cuando realmente se necesitan. Permite trabajar con estructuras potencialmente infinitas y optimiza el rendimiento.
class ExpensiveResource {
val data: String by lazy {
println("Calculando datos...")
"Datos calculados" // Solo se ejecuta la primera vez
}
}
Kotlin proporciona la clase Sequence que permite trabajar con colecciones de manera perezosa. Las operaciones sobre secuencias no se ejecutan hasta que se necesita el resultado final, lo que puede mejorar el rendimiento y reducir el uso de memoria.
val numeros = generateSequence(1) { it + 1 } // Secuencia infinita
val primerosCinco = numeros.take(5).toList() // Solo calcula los primeros 5
// Vs eager evaluation con listas
val numerosEager = (1..1000000).map { it * 2 }.filter { it > 100 } // Calcula todo de una vez
val numerosLazy = (1..1000000).asSequence().map { it * 2 }.filter { it > 100 }.take(10).toList() // Calcula solo lo necesario
println(numerosEager.size) // Procesa toda la lista antes de poder usarla
println(numerosLazy) // Solo calcula los primeros 10 elementos que cumplan la condición
En lugar de usar estructuras de control tradicionales, se combinan funciones pequeñas para crear funciones más complejas.
fun String.esPalindromo(): Boolean = this == this.reversed()
fun String.limpiar(): String = this.trim().lowercase()
fun validarPalabra(palabra: String): Boolean {
return palabra.limpiar().esPalindromo()
}
En el siguiente ejemplo, se muestra cómo componer varias funciones lambda para procesar un texto:
val procesarTexto = { texto: String ->
texto.trim()
.lowercase()
.split(" ")
.filter { it.isNotEmpty() }
.map { it.replaceFirstChar { c -> c.uppercase() } }
.joinToString(" ")
}
La función procesarTexto toma una cadena de texto, la limpia, la convierte a minúsculas, la divide en palabras, filtra las palabras vacías, capitaliza cada palabra y finalmente las une en una sola cadena.
Son métodos que aplican los principios de programación funcional para trabajar con colecciones (listas, arrays, etc.) de manera más elegante y eficiente.
Características principales:
Las más importantes:
map - Transformaciónval numeros = listOf(1, 2, 3, 4, 5)
val cuadrados = numeros.map { it * it } // [1, 4, 9, 16, 25]
filter - Filtradoval pares = numeros.filter { it % 2 == 0 } // [2, 4]
reduce y fold - Reducciónval suma = numeros.reduce { acc, n -> acc + n } // 15
val sumaConInicial = numeros.fold(10) { acc, n -> acc + n } // 25
flatMap - Aplanamientoval palabras = listOf("hola mundo", "kotlin funcional")
val todasLasPalabras = palabras.flatMap { it.split(" ") }
// ["hola", "mundo", "kotlin", "funcional"]
groupBy - Agrupamientodata class Estudiante(val nombre: String, val carrera: String)
val estudiantes = listOf(
Estudiante("Ana", "Sistemas"),
Estudiante("Luis", "Civil"),
Estudiante("María", "Sistemas"),
Estudiante("Pedro", "Civil")
)
val porCarrera = estudiantes.groupBy { it.carrera }
// {Sistemas=[Ana, María], Civil=[Luis, Pedro]}
sortedBy y sortedByDescending - Ordenamientoval nombres = listOf("Carlos", "Ana", "Beatriz", "David")
val ordenados = nombres.sortedBy { it.length } // [Ana, David, Carlos, Beatriz]
val descendente = nombres.sortedByDescending { it } // [David, Carlos, Beatriz, Ana]
any, all, none - Verificaciónval numeros = listOf(1, 2, 3, 4, 5)
numeros.any { it > 3 } // true - ¿alguno es mayor a 3?
numeros.all { it > 0 } // true - ¿todos son mayores a 0?
numeros.none { it < 0 } // true - ¿ninguno es menor a 0?
zip - Combinar dos listasval nombres2 = listOf("Ana", "Luis", "María")
val edades = listOf(20, 22, 21)
val parejas = nombres2.zip(edades)
// [(Ana, 20), (Luis, 22), (María, 21)]
val presentaciones = nombres2.zip(edades) { nombre, edad -> "$nombre tiene $edad años" }
// [Ana tiene 20 años, Luis tiene 22 años, María tiene 21 años]
takeWhile y dropWhileval numeros2 = listOf(1, 2, 3, 4, 5, 1, 2)
val tomados = numeros2.takeWhile { it < 4 } // [1, 2, 3] - toma mientras se cumpla
val restantes = numeros2.dropWhile { it < 4 } // [4, 5, 1, 2] - descarta mientras se cumpla
En la práctica, las operaciones funcionales se encadenan para resolver problemas complejos de forma declarativa:
data class Venta(val vendedor: String, val producto: String, val monto: Double)
val ventas = listOf(
Venta("Ana", "Laptop", 2500.0),
Venta("Luis", "Mouse", 50.0),
Venta("Ana", "Teclado", 150.0),
Venta("Luis", "Monitor", 800.0),
Venta("Ana", "USB", 20.0)
)
val resumen = ventas
.filter { it.monto > 100 }
.groupBy { it.vendedor }
.map { (vendedor, ventas) ->
"$vendedor: ${ventas.size} ventas por $${ventas.sumOf { it.monto }}"
}
.sortedBy { it }
// [Ana: 2 ventas por $2650.0, Luis: 1 ventas por $800.0]
Kotlin incluye varias funciones de alcance: apply, let, also, with, run que simplifican la escritura de código más conciso, legible y expresivo.
Estas funciones permiten ejecutar bloques de código en un contexto determinado, evitando variables temporales innecesarias, facilitando la inicialización de objetos y promoviendo un estilo de programación más funcional.
Por ejemplo, considere el siguiente código para inicializar un objeto Persona:
data class Persona(val nombre: String, val edad: Int)
applySe usa para configurar e inicializar objetos. El objeto receptor es this dentro del bloque. Retorna el mismo objeto, lo que permite encadenar configuraciones. Es especialmente útil con objetos mutables:
val configuracion = StringBuilder().apply {
append("Nombre: Juan")
append(", Edad: 30")
}
println(configuracion) // Nombre: Juan, Edad: 30
letSe usa para ejecutar un bloque de código con un objeto no nulo. El objeto receptor es it dentro del bloque.
val nombre: String? = "Carlos"
nombre?.let {
println("El nombre es $it") // Solo se ejecuta si nombre no es nulo
}
alsoSe usa para realizar acciones adicionales con un objeto sin modificarlo. El objeto receptor es it dentro del bloque. Retorna el mismo objeto original.
val numeros = mutableListOf(1, 2, 3).also {
println("Lista creada con ${it.size} elementos: $it")
}
// Imprime: Lista creada con 3 elementos: [1, 2, 3]
withSe usa para ejecutar un bloque de código con un objeto específico. El objeto receptor es this dentro del bloque.
val persona = Persona("Ana", 25)
with(persona) {
println("Nombre: $nombre, Edad: $edad") // Sin necesidad de usar 'persona.'
}
runSe usa para ejecutar un bloque de código y devolver un resultado. El objeto receptor es this dentro del bloque.
val resultado = persona.run {
"Nombre: $nombre, Edad: $edad"
}
println(resultado) // Nombre: Ana, Edad: 25
| Función | Referencia al objeto | Valor de retorno | Uso principal |
|---|---|---|---|
apply |
this |
El mismo objeto | Configurar propiedades de un objeto |
let |
it |
Resultado del lambda | Ejecutar código con objetos no nulos |
also |
it |
El mismo objeto | Acciones adicionales (logging, validación) |
with |
this |
Resultado del lambda | Agrupar llamadas sobre un mismo objeto |
run |
this |
Resultado del lambda | Configurar objeto y calcular un resultado |
Refactorice los siguientes fragmentos de código imperativo a un estilo funcional usando las operaciones sobre colecciones aprendidas (map, filter, groupBy, reduce, etc.):
Fragmento A:
val nombres = listOf("ana", "CARLOS", " beatriz ", "DAVID", "elena")
val resultado = mutableListOf<String>()
for (nombre in nombres) {
val limpio = nombre.trim().lowercase()
if (limpio.length > 4) {
resultado.add(limpio.replaceFirstChar { it.uppercase() })
}
}
println(resultado)
Fragmento B:
data class Estudiante(val nombre: String, val nota: Double, val materia: String)
val estudiantes = listOf(
Estudiante("Ana", 4.5, "Matemáticas"),
Estudiante("Luis", 3.2, "Programación"),
Estudiante("María", 4.8, "Matemáticas"),
Estudiante("Pedro", 2.9, "Programación"),
Estudiante("Sofía", 3.7, "Matemáticas")
)
// Calcular el promedio de notas por materia (solo de estudiantes aprobados con nota >= 3.0)
var sumaMatematicas = 0.0
var contadorMatematicas = 0
var sumaProgramacion = 0.0
var contadorProgramacion = 0
for (e in estudiantes) {
if (e.nota >= 3.0) {
if (e.materia == "Matemáticas") {
sumaMatematicas += e.nota
contadorMatematicas++
} else {
sumaProgramacion += e.nota
contadorProgramacion++
}
}
}
Cree un ejemplo práctico que use las cinco funciones de ámbito (apply, let, also, with, run) en un solo programa que modele una aplicación de gestión de usuarios.