Programación funcional en Swift: nivel avanzado
\n\nLa programación funcional (PF) es un paradigma que considera los cálculos como la evaluación de funciones matemáticas y evita el cambio de estado y los datos mutables. En Swift, que es un lenguaje multiparadigma, las capacidades funcionales no solo son compatibles, sino que están profundamente integradas en el lenguaje y la biblioteca estándar. Si ya domina conceptos básicos como map, filter y reduce, es hora de profundizar en técnicas avanzadas que le ayudarán a escribir código más seguro, predecible y comprobable para aplicaciones iOS y macOS. En este artículo, exploraremos functores, mónadas, evaluación perezosa y composición de funciones con ejemplos prácticos.
1. Functores: contenedores con capacidad de transformación
\n\nUn functor es un tipo de datos que implementa la función fmap (en Swift, map). La esencia de un functor es que representa un contenedor cuyo contenido se puede transformar mediante una función pasada, sin salir del contenedor. Un ejemplo clásico es Optional.
Veamos cómo funciona un functor con el ejemplo de Optional:
let maybeNumber: Int? = 5\nlet result = maybeNumber.map { $0 * 2 }\nprint(result) // Optional(10)\n\nlet nilValue: Int? = nil\nlet nilResult = nilValue.map { $0 * 2 }\nprint(nilResult) // nil\n\nAquí, Optional es un functor. Si hay un valor dentro, map aplica el cierre y devuelve un nuevo Optional. Si no hay valor, la operación no se ejecuta y obtenemos nil. Esto evita construcciones engorrosas con if let.
Otro functor importante en Swift es Array. map en un arreglo aplica una función a cada elemento y devuelve un nuevo arreglo:
let numbers = [1, 2, 3, 4]\nlet doubled = numbers.map { $0 * 2 }\nprint(doubled) // [2, 4, 6, 8]\n\nCreando su propio functor. Puede implementar un functor para cualquier tipo personalizado definiendo el método map. Por ejemplo, un envoltorio simple para registro:
struct Logged<T> {\n let value: T\n let log: [String]\n \n func map<U>(_ transform: (T) -> U) -> Logged<U> {\n let newValue = transform(value)\n return Logged<U>(value: newValue, log: log + ["Transformed \\(value) to \\(newValue)"])\n }\n}\n\nlet logged = Logged(value: 10, log: ["Initial value"])\nlet transformed = logged.map { $0 + 5 }\nprint(transformed.log) // ["Initial value", "Transformed 10 to 15"]\n\n2. Mónadas: cadenas de cálculos con contexto
\n\nUna mónada es un functor que además implementa el método flatMap (o bind). Mientras que map permite aplicar una función que devuelve un valor normal, flatMap permite aplicar una función que a su vez devuelve un valor en el mismo contexto (por ejemplo, otro Optional). Esta es una herramienta clave para construir cadenas de operaciones manteniendo el contexto.
El ejemplo más claro es trabajar con valores opcionales. Supongamos que tenemos una función que extrae un número de una cadena y otra función que divide un número por otro, pero ambas pueden devolver nil:
func parseNumber(_ string: String) -> Int? {\n return Int(string)\n}\n\nfunc divideSafely(_ a: Int, by b: Int) -> Int? {\n guard b != 0 else { return nil }\n return a / b\n}\n\n// Sin flatMap hay que usar if let anidados\nif let num1 = parseNumber("10") {\n if let result = divideSafely(num1, by: 2) {\n print(result) // 5\n }\n}\n\n// Con flatMap todo se vuelve más elegante\nlet result = parseNumber("10").flatMap { divideSafely($0, by: 2) }\nprint(result) // Optional(5)\n\nlet failed = parseNumber("abc").flatM