En artículos ateriores habíamos visto cóm omejorar la arquiectura de nuestro código mediante patrones de diseño y el uso de los principios SOLID. Ahora veremos cómo mejorar el código Swift con funciones de orden superior. Seguramente más de una vez las has utilizado, pero, ¿qué son y cómo mejora tu código Swift con funciones de orden superior?
Funciones de orden superior en Swift
Las funciones de orden superior son funciones que toman como argumentos otras funciones o cierres (closures) y que devuelven una función o un cierre (closure). Estas funciones se utilizan con arrays, sets y dictionaries, y actúan sobre los elementos que contienen (esto se hace mediante métodos que se aplicación sobre lo elementos de la colección mediante la sintaxis de punto).
Algunas de las funciones más conocidas son map, compactMap, flatMap, reduce, filter, contains, sorted, forEach, o removeAll.
map
La función map actúa realizando una operación sobre todos los elementos de una colección y devolviendo una nueva colección con los resultados de dicha operación.
Por ejemplo, vamos a suponer que tenemos un array con varias palabras con todas sus letras en minúscula y queremos obtener un nuevo array con cada una de estas palabras pero con todas las letras en mayúscula. Esto lo podríamos hacer con un bucle for…in:
1 2 3 4 5 6 7 8 |
let words: [String] = ["room", "home", "train", "green", "heroe"] var uppercasedWords: [String] = [String]() for word in words { uppercasedWords.append(word.uppercased()) } // uppercasedWords = ["ROOM", "HOME", "TRAIN", "GREEN", "HEROE"] |
Vamos a ver cómo hacerlo con la función map. Tal como se muestra en la documentación de Apple, map se declara como:
1 |
func map<T>(_ transform: (Element) throws -> T) rethrows -> [T] |
Donde transform acepta un elemento de una colección o secuencia como parámetro y devuelve un valor transformado del mismo tipo o de otro diferente.
En el ejemplo que estamos viendom lo aplicamos de la siguiente manera:
1 2 3 4 5 |
let words: [String] = ["room", "home", "train", "green", "heroe"] let uppercasedWords = words.map({ word in return word.uppercased() }) |
Lo que hace map es recorrer todo el array de elementos, applicar el método uppercased() en cada uno de ellos y devolver un nuevo array con estos valores.
De todas formas, podemos reducir esta expresión haciendo uso del argumento abreviado $0, con el cual se hace referencia a cualquier elementos del array:
1 |
let uppercasedWords = words.map({ $0.uppercased() }) |
compactMap
Supongamos ahora que dentro del array del ejemplo anterior hay valores nulos (nil). Si utilizamos la función map, deberemos tener en cuenta si el valor sobre el que se actúa es nulo (nil) o no lo es:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
let words: [String?] = ["room", "home", nil, nil, "train", nil, nil, "green", "heroe", nil] let uppercasedWords = words.map({ word -> String? in if let word = word { return word.uppercased() } else { return nil } }) // O de forma reducida: let uppercasedWords = words.map { $0 != nil ? $0!.uppercased() : nil } // uppercasedWords = ["ROOM", "HOME", nil, nil, "TRAIN", nil, nil, "GREEN", "HEROE", nil] |
Pero, ¿y si lo que queremos en realidad es obtener el nuevo array pero sin los valores nulos? Para conseguir esto tenemos la función compactMap.
La funció compactMap devuelve un array que contiene los resultados no nulos (nil) tras aplicar la transformación dada a cada elemento de una secuencia.
1 |
func compactMap<ElementOfResult>(_ transform: (Element) throws -> ElementOfResult?) rethrows -> [ElementOfResult] |
Por tanto, en el ejemplo que estamos viendo:
1 2 3 |
let uppercasedWords = words.compactMap { $0.uppercased() } // uppercasedWords = ["ROOM", "HOME", "TRAIN", "GREEN", "HEROE"] |
Es decir, compactMap recorre todos los elementos del array y aplica el método a los valores no nulos, devolviéndolos en un array, en este caso del tipo [String] (es decir, el valor de String no es opcional).
flatMap
La función flatMap nos permite transformar un array de arrays en un único array que contiene todos los elementos. Tal como lo declara Apple en su documentación:
1 |
func flatMap<SegmentOfResult>(_ transform: (Self.Element) throws -> SegmentOfResult) rethrows -> [SegmentOfResult.Element] where SegmentOfResult : Sequence |
Por ejemplo, veamos primero cómo lo haríamos sin flatMap:
1 2 3 4 5 6 7 8 9 10 |
let words: [[String]] = [["room", "home"], ["train", "green"], ["heroe"]] var singleArray: [String] = [String]() for individualArray in words { for word in individualArray { singleArray.append(word) } } // singleArray = ["room", "home", "train", "green", "heroe"] |
Pero con flatMap podemos simplificar el código de la siguiente manera:
1 2 3 4 5 |
let words: [[String]] = [["room", "home"], ["train", "green"], ["heroe"]] let singleArray = words.flatMap { $0 } // singleArray = ["room", "home", "train", "green", "heroe"] |
reduce
reduce es una función que, al aplicarla sobre una colección, nos devuelve el resultado de combinar los elementos de dicha colección:
1 |
func reduce<Result>(_ initialResult: Result, _ nextPartialResult: (Result, Element) throws -> Result) rethrows -> Result |
Por ejemplo, si tenemos un array con los números del 1 al 10 y queremos obtener sus suma. Podemos hacerlo de la siguiente forma sin utilizar la función reduce:
1 2 3 4 5 6 7 8 9 |
let numbers: [Int] = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10] var result: Int = 0 for number in numbers { result += number } // result = 55 |
Con reduce, simplemente hacemos lo siguiente:
1 2 3 4 5 6 7 |
let numbers: [Int] = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10] let sum = numbers.reduce(0, { x, y in return x + y }) // sum = 55 |
En la primera iteración el valor de x es 0 (tal como lo hemos indicado en la función) y el valor de y es 1. Por lo que el resultado x + y será 1. En la segunda iteración x es 1 e y es 2, por lo que el resultado será 3. Y así sucesivamente.
De una forma más simplificada podemos escribir:
1 |
let sum = numbers.reduce(0, { $0 + $1 }) |
filter
Tal como su nombre indica, la función filter filtra el contenido de una colección y devuelve una nueva colección que contiente lo elementos que cumplen una determinada condición:
1 |
func filter(_ isIncluded: (Self.Element) throws -> Bool) rethrows -> [Self.Element] |
Por ejemplo, supongamos que tenemos la colección de palabras de los primeros ejemplos y queremos obtener una nueva colección con las palabras que contengan la letra ‘o’. Sin la función filter, lo podríamos hacer de la siguiente forma:
1 2 3 4 5 6 7 8 9 10 11 12 |
let words: [String] = ["room", "home", "train", "green", "heroe"] var wordsWithO: [String] = [String]() for word in words { for letter in word { if letter == "o" { wordsWithO.append(word) break } } } |
En este caso, no hemos utilizado la funcion contains ya que como veremos más adelante, también es una funcion de orden superior.
Ahora vamos a simplificar este código usando la función filter:
1 2 3 4 5 |
let words: [String] = ["room", "home", "train", "green", "heroe"] let wordsWithO = words.filter { $0.contains("o") } // wordsWithO = ["room", "home", "heroe"] |
Pero no tiene porqué aplicarse una única condición, podemos aplicar varias condiciones. Por ejemplo, en el caso anterior podemos hacer que nos devuelva las palabras que contengan la vocal ‘o’ y cuya longitud sea de 5 caracteres o más:
1 2 3 4 5 |
let words: [String] = ["room", "home", "train", "green", "heroe"] let worthWithO = words.filter { $0.contains("o") && $0.count >= 5 } // worthWithO = ["heroe"] |
contains
En el ejemplo anterior hemos utilizado la función contains para determinar si una palabra contenía la vocal ‘o’. Pues bien, contains es un función de orden superior que permite comprobar si hay elementos que cumplen una determinada condición y devolver true o false según la cumpla o no.
Tal como indica Apple en su documentación, contains devuelve un valor booleano que indica si la secuencia contiene el elemento dado.
1 |
func contains(_ element: Element) -> Bool |
sorted
En numerosas ocasiones nos encontramos con una colección de elementos que queremos ordenar de alguna manera para mostrarlos. Por ejemplo, en los ejemplos del array de palabras vistos hasta ahora, dichas palabras no están ordenadas alfabéticamente.
Pero, ¿y si las quisieramos ordenar alfabéticamente? Podríamos utilizar algún algoritmo, como:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
var words: [String] = ["room", "home", "train", "green", "heroe"] var swapWord: Bool = false repeat { swapWord = false for n in 0...words.count - 2 { if words[n] > words[n + 1] { let temporalWord = words[n + 1] words[n + 1] = words[n] words[n] = temporalWord swapWord = true } } } while swapWord // words = ["green", "heroe", "home", "room", "train"] |
Este código lo podrímaos reducir utilizando el método swapAt, que nos permite intercamiar las posiciones de dos elementos en una secuencia:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
var words: [String] = ["room", "home", "train", "green", "heroe"] var swapWord: Bool = false repeat { swapWord = false for n in 0...words.count - 2 { if words[n] > words[n + 1] { words.swapAt(n, n + 1) swapWord = true } } } while swapWord // words = ["green", "heroe", "home", "room", "train"] |
Para reducir más este código, podemos utilizar la función sorted. Esta función nos devuelve los elementos de una secuencia ordenados de forma ascendente (siempre y cuando los elementos de la colección adopten el protocolo Comparable):
1 |
func sorted() -> [Element] |
1 2 3 4 5 |
let words: [String] = ["room", "home", "train", "green", "heroe"] let sortedWords = words.sorted() // sortedWords = ["green", "heroe", "home", "room", "train"] |
Por otro lado, si queremos utilizar nuestra propia condición para ordenar la colección, utilizamos la función sorted(by:), que tal como indica Apple en su documentación, devuelve los elementos de la secuencia, ordenados usando el predicado dado como la comparación entre elementos.
1 |
func sorted(by areInIncreasingOrder: (Element, Element) throws -> Bool) rethrows -> [Element] |
Por ejemplo, si queremos que las palabras estén ordenadas de orden alfabético inverso podemos hacer lo siguiente:
1 2 3 4 5 |
let words: [String] = ["room", "home", "train", "green", "heroe"] let sortedWords = words.sorted(by: > ) //sortedWords = ["train", "room", "home", "heroe", "green"] |
En este caso, al indicar el símbolo ‘>’ ordenamos la colección en orden descendente.
forEach
Cumple una función similar a for…in, pero a diferencia de este, no se puede utilizar ni continue ni break dentro de forEach, solo return:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
let words: [String] = ["room", "home", "train", "green", "heroe"] words.forEach({ word in guard word.count > 4 else { print(word.uppercased()) return } print(word) }) // ROOM // HOME // train // green // heroe |
removeAll
La función de orden superior removeAll(where:) nos permite eliminar los elementos de una secuencia que cumplan ciertas condiciones:
1 |
mutating func removeAll(where shouldBeRemoved: (Element) throws -> Bool) rethrows |
Por ejemplo, si queremos eliminar todos los números pares de una secuencia, podemos hacer lo siguiente mediante removeAll:
1 2 3 4 5 |
var numbers: [Int] = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10] numbers.removeAll(where: { $0 % 2 == 0 }) // numbers = [2, 4, 6, 8, 10] |
La potencia de removeAll(where:) se ve de forma más clara en el ejemplo que muestra Apple en su documentación, en el que lo utiliza para eliminar las vocales de una frase:
1 2 3 4 5 6 7 |
var phrase = "The rain in Spain stays mainly in the plain." let vowels: Set<Character> = ["a", "e", "i", "o", "u"] phrase.removeAll(where: { vowels.contains($0) }) // phrase == "Th rn n Spn stys mnly n th pln." |
Concatenación de funciones
Las funciones de primer orden se pueden aplicar de forma consecutiva, concatenda. Por ejemplo, podemos tomar un array que contenga arrays de números y calcular sus suma:
1 2 3 4 5 6 7 |
let numbers: [[Int]] = [[1, 3, 6, 2], [2, 5, 7], [1, 3]] let sum: Int = numbers .flatMap({ $0 }) .reduce(0, {$0 + $1}) // sum = 30 |
Primero aplicamos la funcion flatMap para obtener un array con todos los números. Luego aplicamos la función reduce para sumarlos.
Conclusiones
Acabamos de ver algunas de las funciones de orden superior más utilizadas y su potencia mediante algunos ejemplos. Estas funciones permiten, por un lado, reducir la cantidad de código y por otro hacerlo más claro y conciso.