Programación funcional en Kotlin: nivel avanzado
\n\nKotlin fue creado originalmente como un lenguaje multiparadigma que combina de manera orgánica los enfoques orientado a objetos y funcional. Gracias a capacidades como funciones de orden superior, expresiones lambda, inmutabilidad de datos (val) y un potente sistema de tipos, Kotlin permite implementar conceptos complejos de programación funcional (FP). En este artículo, exploraremos técnicas avanzadas de FP en Kotlin que le ayudarán a escribir código más limpio, predecible y comprobable.
\n\n1. Functores y functores aplicativos
\n\nUn functor es una estructura de datos que admite la operación map, la cual aplica una función al contenido de la estructura manteniendo su forma. En Kotlin, los functores estándar son las colecciones y tipos similares a Option (por ejemplo, T? o Result<T>).
Implementación de un functor para Result
\nConsideremos un ejemplo con el tipo Result, que encapsula un valor exitoso o un error. Podemos crear un functor implementando el método map:
sealed class Result<out T> {\n data class Success<T>(val value: T) : Result<T>()\n data class Error(val exception: Throwable) : Result<Nothing>()\n}\n\nfun <T, R> Result<T>.map(f: (T) -> R): Result<R> = when (this) {\n is Result.Success -> Result.Success(f(value))\n is Result.Error -> this // conservamos el error\n}\n\n// Uso\nval result: Result<Int> = Result.Success(5)\nval mapped = result.map { it * 2 } // Result.Success(10)\n\n\nUn functor aplicativo extiende las capacidades de un functor normal: permite aplicar una función que está dentro de un contexto (por ejemplo, Result) a un valor dentro de un contexto. Para ello se utiliza el método ap.
fun <T, R> Result<(T) -> R>.ap(value: Result<T>): Result<R> = when (this) {\n is Result.Success -> value.map(f)\n is Result.Error -> this\n}\n\n// Ejemplo: combinamos dos resultados\nval add: (Int) -> (Int) -> Int = { a -> { b -> a + b } }\nval resultF: Result<(Int) -> (Int) -> Int> = Result.Success(add)\nval resultA: Result<Int> = Result.Success(3)\nval resultB: Result<Int> = Result.Success(7)\nval combined = resultF.ap(resultA).ap(resultB) // Result.Success(10)\n\n\n2. Mónadas: Option, Either y trabajo con efectos secundarios
\n\nUna mónada es un tipo que implementa dos operaciones: unit (colocar un valor en un contexto) y flatMap (encadenar operaciones monádicas). En Kotlin, una mónada incorporada puede considerarse T? (tipo nullable), donde unit es simplemente el valor y flatMap es el operador ?.let o fold.
Implementación de la mónada Option
\n\nsealed class Option<out T> {\n data class Some<T>(val value: T) : Option<T>()\n object None : Option<Nothing>()\n}\n\nfun <T, R> Option<T>.flatMap(f: (T) -> Option<R>): Option<R> = when (this) {\n is Option.Some -> f(value)\n is Option.None -> Option.None\n}\n\nfun <T> T.toOption(): Option<T> = Option.Some(this)\n\n// Ejemplo de cadena de operaciones\ndata class User(val id: Int, val name: String)\n\nfun findUser(id: Int): Option<User> = if (id == 1) Option.Some(User(1, "Alice")) else Option.None\nfun getEmail(user: User): Option<String> = Option.Some("${user.name}@example.com")\n\nval email = findUser(1).flatMap { getEmail(it) } // Option.Some("Alice@example.com")\n\n\nMónada Either para el manejo de errores
\nA diferencia de Option, Either almacena información sobre el error:
sealed class Either<out L, out R> {\n data class Left<L>(val value: L) : Either<L, Nothing>()\n