Programación funcional en Kotlin: nivel avanzado
Kotlin es un lenguaje multiparadigma que combina de manera orgánica los enfoques orientado a objetos y funcional. Si ya dominas los conceptos básicos de la programación funcional (PF), como map, filter, fold y lambdas, es hora de sumergirte en técnicas avanzadas. En este artículo analizaremos la composición de funciones, el currying, la evaluación perezosa y también conoceremos los functores y mónadas con ejemplos en Kotlin. Todos los ejemplos de código se pueden ejecutar en tu IDE.
1. Composición de funciones y currying
La composición es la creación de una nueva función mediante la aplicación secuencial de dos o más funciones. Kotlin no tiene un operador de composición incorporado como Haskell, pero se puede implementar fácilmente mediante funciones de extensión.
Ejemplo de composición de funciones
// Definimos las funcionesval double: (Int) -> Int = { it * 2 }val increment: (Int) -> Int = { it + 1 }
// Composición: primero double, luego incrementinfix fun <A, B, C> ((B) -> C).compose(f: (A) -> B): (A) -> C = { a -> this(f(a)) }
val doubleThenIncrement = increment compose doubleprintln(doubleThenIncrement(5)) // 11 (5*2 + 1)Currying
El currying es la transformación de una función de varios argumentos en una cadena de funciones, cada una de un solo argumento. En Kotlin esto se implementa mediante lambdas anidadas.
// Función normalfun sum(a: Int, b: Int): Int = a + b
// Versión currificadafun curriedSum(a: Int): (Int) -> Int = { b -> a + b }
val add5 = curriedSum(5)println(add5(3)) // 8
// Función universal para curryingfun <A, B, C> ((A, B) -> C).curry(): (A) -> (B) -> C = { a -> { b -> this(a, b) } }
val curriedSum2 = ::sum.curry()println(curriedSum2(10)(20)) // 30El currying es especialmente útil al trabajar con aplicación parcial y fábricas de funciones.
2. Functores y mónadas: teoría en la práctica
Un functor es un contenedor que se puede mapear (map) mediante una función. Una mónada es un functor con la capacidad de componer contenedores (flatMap). En Kotlin, un ejemplo claro de mónada es Result (o Either), así como Sequence.
Ejemplo con Result (mónada Either)
data class User(val name: String, val email: String)
fun parseUser(json: String): Result<User> = runCatching { // Simulación de parseo if (json.isBlank()) throw IllegalArgumentException("Empty JSON") User("Alice", "alice@example.com")}
fun sendEmail(user: User): Result<String> = runCatching { if (user.email.contains("@")) "Email sent to ${user.email}" else throw IllegalArgumentException("Invalid email")}
// Cadena monádicaval result = parseUser("{...}") .flatMap { user -> sendEmail(user) } .map { message -> "Success: $message" } .onFailure { error -> println("Error: ${error.message}") }
println(result.getOrDefault("Failed"))Aquí flatMap permite evitar comprobaciones anidadas de éxito/error, haciendo que el código sea lineal y legible.
Creación de nuestro propio functor: tipo Option
sealed class Option<out T> { data class Some<T>(val value: T) : Option<T>() object None : Option<Nothing>()}
fun <T, R> Option<T>.map(f: (T) -> R): Option<R> = when (this) { is Option.Some -> Option.Some(f(value)) is Option.None -> Option.None}
fun <T, R> Option<T>.flatMap(f: (T) -> Option<R>): Option<R> = when (this) { is Option.Some -> f(value) is Option.None -> Option.None}
val maybeNumber: Option<Int> = Option.Some(42)val resultOption = maybeNumber .map { it * 2 } .flatMap { Option.Some("Value: $it") }
println(resultOption) // Some(value=Value: 84)