Swift y Objective-C

[Swift intermedio #5] Funciones de orden superior

Las cinco funciones map·filter·compactMap·flatMap·reduce se distinguen por lo que devuelve el cierre. Aquí se explican los criterios para elegirlas frente a bucles for, los arrays intermedios del encadenamiento y los problemas que resuelven las secuencias lazy.

6 min de lectura
Imagen de portada de [Swift intermedio #5] Funciones de orden superior

Todo el mundo usa map y filter. El problema llega después. compactMap y flatMap confunden por sus nombres parecidos, mientras que reduce se evita porque su firma intimida. Si encadenas demasiadas operaciones, aparece una inquietud de rendimiento: «¿Cuántos arrays nuevos está creando esto?»

La quinta entrega de la serie intermedia es una guía práctica de las funciones de orden superior. Cubre la diferencia exacta entre las cinco funciones, cuándo elegirlas frente a bucles for y qué problemas resuelven las secuencias lazy. Es la parte práctica construida sobre los fundamentos del capítulo de cierres.

El lugar exacto de cada función: lo que revela su firma

Una función de orden superior recibe otra función como argumento. Las cinco funciones principales para arrays se distinguen por la firma del cierre; concretamente, por en qué se convierte un elemento.

map: elemento → elemento nuevo. Mantiene la cantidad. Si entran n elementos, salen n. Solo transforma; no filtra.

filter: elemento → Bool. La cantidad puede disminuir; los elementos no cambian. Conserva solo los elementos que cumplen la condición y no transforma nada.

compactMap: elemento → opcional. Descarta nil y devuelve el valor desempaquetado. Es una combinación especializada de map y filter, útil cuando una transformación puede fallar.

let inputs = ["1", "2", "tres", "4"]
let numbers = inputs.compactMap { Int($0) }  // [1, 2, 4]

Int(“tres”) produce nil y compactMap lo filtra. Si se hubiera usado map { Int($0) }, habría producido [Int?], repitiendo el tratamiento de opcionales en cada uso. compactMap aplica a colecciones el principio del capítulo de opcionales: «desempaqueta el opcional en el límite».

flatMap: elemento → array. Aplana un nivel de anidamiento. Se usa cuando un elemento se expande en varios. Si sentences.flatMap { $0.split(separator: " ") } convierte un array de frases en un array de palabras, map produciría [[단어]], mientras que flatMap elimina un nivel. La diferencia con compactMap es sencilla: si el cierre devuelve un opcional, usa compactMap; si devuelve un array o una secuencia, usa flatMap.

reduce: colección completa → un valor. Es la forma general de plegarlo todo en un resultado: una suma, un máximo o un diccionario. reduce(0, +) calcula una suma; su primer argumento es el valor inicial y el segundo indica cómo combinar el resultado acumulado con el elemento siguiente. Recuerda este consejo de rendimiento: al acumular un array o un diccionario, usa reduce(into:). reduce copia el acumulador en cada paso; la variante into modifica continuamente uno solo, algo importante con muchos datos.

Bucles for frente a funciones de orden superior: la realidad de la legibilidad

La frase «usar funciones de orden superior en lugar de bucles for es más propio de Swift» es mitad correcta y mitad peligrosa. Estos son los criterios.

La ventaja real de las funciones de orden superior no es que sean breves, sino que declaran la intención. Al ver filter, el lector sabe por la firma que se está filtrando y que los elementos no cambiarán. Con un bucle for debe leer todo el cuerpo para llegar a esa conclusión. Es un ejemplo de la expresividad tratada en la primera parte: hacer visible la intención en el código.

Pero esa ventaja solo se mantiene cuando cada paso es simple. Si el cierre contiene tres niveles de condiciones y efectos secundarios —modificar una variable externa o llamar a print—, se traiciona la expectativa de que filter solo filtra. El resultado puede ser más difícil de leer que un for, en contra del principio de mínima sorpresa. Los criterios son:

  • Combinaciones simples de transformación, filtrado y agregación → funciones de orden superior.
  • Ramificación compleja en cada paso, efectos secundarios o salida intermedia (break) → bucle for. Las funciones de orden superior no tienen un equivalente adecuado de break: first(where:) sirve para detenerse en el primer elemento que cumple la condición, pero un bucle resulta más natural para una lógica de salida anticipada más compleja.
  • Trabajos que necesitan un índice → también pueden usar una función de orden superior con enumerated(), pero si se complica, usa un bucle.

Hay un antipatrón que conviene dejar claro: hacer append en un array externo dentro de forEach. var result: [Int] = []; items.forEach { result.append($0 * 2) } reimplementa manualmente lo que map hace en una línea y solo añade estado mutable. forEach encaja con efectos secundarios por elemento, como enviar notificaciones; si se está creando algo, corresponde una función de la familia map.

Diagrama que contrasta un encadenamiento que acumula arrays intermedios con una tubería lazy que deja pasar los elementos de uno en uno
El encadenamiento crea un array intermedio en cada etapa; lazy procesa los elementos de uno en uno

El coste del encadenamiento: el invitado oculto llamado array intermedio

Encadenar funciones de orden superior parece elegante, pero cada etapa crea un array nuevo.

let result = products
    .filter { $0.inStock }      // array intermedio 1
    .map { $0.price }           // array intermedio 2
    .prefix(5)                  // final

Con un millón de elementos, filter puede crear un array de hasta un millón de elementos y map crea otro. Aunque solo necesites los cinco primeros, recorres y asignas memoria dos veces.

lazy es la solución estándar. Al insertar lazy como en products.lazy.filter{...}.map{...}.prefix(5), cambia la naturaleza de la tubería. En lugar de ejecutarse de inmediato y crear arrays, cada etapa queda registrada en una secuencia diferida. Al consumirla finalmente, cada elemento atraviesa toda la tubería de uno en uno. La iteración se detiene al reunir cinco, así que solo se toca la parte inicial necesaria del millón. Tampoco hay arrays intermedios.

Eso no significa que debas usar siempre lazy. Como no almacena resultados, consumir dos veces la misma secuencia diferida ejecuta dos veces el cálculo. Además, como los cierres se almacenan por referencia y se ejecutan después, pueden aparecer restricciones relacionadas con escaping. La regla es simple: lazy brilla con datos grandes y consumo parcial, como la combinación prefix, first(where:). Si vas a consumirlo todo y guardarlo como array, suele ser mejor la ejecución inmediata. Es la versión para colecciones de la misma pregunta que plantea una propiedad lazy var: «¿Conviene retrasar el cálculo?»

Recetas de combinaciones prácticas

Aquí tienes algunas combinaciones frecuentes a modo de recetas.

Crear un diccionario. Usa Dictionary(uniqueKeysWithValues: users.map { ($0.id, $0) }) para crear una tabla de consulta por ID. Si puede haber claves duplicadas, Dictionary(grouping: orders, by: { $0.customerID }) es la variante de agrupación.

Paso seguro de opcionales. Convierte el array de IDs de tipo String de una respuesta del servidor en un array de URL: ids.compactMap { URL(string: $0) }. Los fallos se filtran silenciosamente y el tipo queda fijado como [URL].

Combinaciones de ordenación. También conviene conocer items.sorted { $0.priority > $1.priority }, que usa un KeyPath en lugar de items.sorted(using: KeyPathComparator(\.priority, order: .reverse)). KeyPath se tratará por separado más adelante.

Agregación. El total del carrito es cart.reduce(0) { $0 + $1.price * Double($1.quantity) }. Para una suma simple, a menudo resulta más legible dividirlo como cart.map(\.subtotal).reduce(0, +). Si el cierre de reduce se complica, es una señal para dividirlo.

Ilustración de un desarrollador leyendo un indicador en la bifurcación entre funciones de orden superior y bucles for
Para transformaciones simples, funciones de orden superior; para ramificación, efectos secundarios o salida anticipada, bucles

Resumen

  • El criterio es lo que devuelve el cierre: elemento nuevo significa map, Bool significa filter, opcional significa compactMap, array significa flatMap y plegar todo en un valor significa reduce (reduce(into:) para colecciones acumuladas).
  • La ventaja de las funciones de orden superior es declarar la intención. Cuando el cierre se complica o incluye efectos secundarios, esa ventaja desaparece y conviene volver al bucle for.
  • El encadenamiento crea un array intermedio en cada etapa. Para consumir solo una parte de datos grandes, crea una tubería diferida con lazy; si consumes todo, mantén la ejecución inmediata.
  • Si estás creando un array con forEach, es un lugar para cambiar a una función de la familia map.

La próxima entrega trata el tema de todo desarrollador de iOS que lucha con JSON: Codable avanzado. Cubre CodingKeys, estructuras anidadas, estrategias de fechas y la solución al problema de que la decodificación completa falle por culpa de un solo campo.

Seguir leyendo