Swift – Closures

En el artículo de hoy aprenderemos sobre los closures, ¿qué es un closure? ¿para qué sirve? ¿cómo se usa? todo esto lo responderemos poco a poco y a través de ejemplos.

Los closures son parte intrínseca del lenguaje Swift, es una área de dominio obligatorio, los closures están en todos lados dentro del lenguaje y se usan extensamente en muchos frameworks con los que trabajamos a diario.

Si quieres convertirte en un desarrollador iOS tienes que aprender y entender los closures, es algo básico.

¿Qué es un closure?

Un closure viene siendo una función anonima de toda la vida, sin nombre, en la cual implementamos una funcionalidad determinada, quedando al final un bloque de código que podemos pasar a otras funciones o retornalo dentro de estas.

La respuesta es bastante sencilla, lo que usualmente no resulta tan simple es entender su utilidad o cuando es el mejor momento para usarlos.

El uso de una característica siempre está determinado por las posibilidades que nos brinda. De esta manera una función cumple su objetivo al ejecutar una tarea, una condición if-else nos ayuda con ciertas desiciones y los bucles a iterar sobre una o varias tareas las cuales pueden estar conformadas por funciones.

Todos estas instrucciones que conforman un lenguaje y que tienen sus características propias, son las que nos permiten expresarnos a través del código, unas interactúan con otras y así vamos construyendo nuestra lógica.

Pues de manera similar sucede con los closures, los matices que los conforman como una característica única del lenguaje, las posibilidades que nos brindan, etc. Todo esto conforma la mejor guía para entender cuando usarlos y mejor aún, aprender a usarlos de manera correcta.

Sintaxis

Los closures cuentan con un estilo bien definido, donde se optimiza y fomenta la limpieza y la claridad del código, con un enfoque bien claro en la flexibilidad.

La sintaxis básica de un closure no es para nada compleja, veamos un ejemplo:

{ (parámetros-de-entrada) -> tipo_de_retorno in declaraciones }

como podemos ver, si apartamos no muy lejos las llaves que lo engloban todo y la ausencia de un nombre, el resto es bastante parecido a la definición de una función. Si no lo vez míralo mejor así:

{ (parámetros-de-entrada) -> tipo_de_retorno in

   declaraciones

}

nótece el uso de la palabra clave in después del tipo de retorno. Esta funge como separador entre el tipo de devolución y las declaraciones dentro del closure.

Ahora, el siguiente paso es concientizar que esto es todo, básicamente. Un closure no es más que este bloque de código. Lo interesante y quizás lo que tiende a confundir es lo que podemos hacer con él y las formas que puede adoptar.

Sí, un closure puede tomar varias formas, como ya he comentado Swift es un lenguaje muy flexible y los closure es uno de los estandartes de esta flexibilidad.

Veamos su forma básica en uso:

let closure = {
    
    (value: Int) -> Int in
    
    return value
    
}

print(closure(20))

aquí podemos ver como el flujo de datos es similar al de una función. El closure recibe un valor entero y devuelve otro valor entero, dentro del código lo único que hacemos es retornar el mismo valor de entrada. Luego este código lo almacenamos en la constante closure.

En la última línea hacemos uso del closure como su fuese una función, le pasamos un valor entero y mediante print mostramos en pantalla el valor de retorno.

Tipo de dato

Un momento, hasta ahora todas las constantes o variables en Swift son de un tipo de dato determinado, ya sea definido de manera explicita o asumido por el compilador en base al valor asociado.

En el ejemplo anterior, ¿Cuál sería el tipo de dato de la constante closure? Pues muy buena pregunta, para conocer el tipo de dato de esta constante solo tendriamos agregar la siguiente línea al ejemplo anterior:

print(type(of: closure))

la salida en pantalla de esta instrucción sería:

(Int) -> Int

y es correcto, el tipo de dato de un closure es lo que en otro lenguajes llamámos la firma de una función o el cabezal (header) de la misma. Luego de esta pintoresca definición podemos analizar la salida y darnos cuenta que el tipo de dato esta conformado por el tipo de entrada, la flecha de retorno y el tipo de salida, así de sencillo.

Ahora, si cambiamos el ejemplo anterior eliminando el valor de retorno:

let closure = {
    
    (value: Int) -> Void in
    
    print(value)
    
}

closure(20)

print(type(of: closure))

El tipo de dato sería:

(Int) -> ()

Ya, y ¿qué pasaría si no recibe nada ni devuelve nada?

let hello = {
    
    () -> Void in
    
    print("Hello!")
    
}

hello()

print(type(of: hello))

Que también sería valido, y el tipo de dato es:

() -> ()

Una función o closure que no recibe nada ni devuelve nada.

¿Cómo es esto posible?

Bueno, de alguna manera tenemos que informar al compilador de que vamos a asociar a esta variable o constante un closure o función, y

¿qué diferencia a una función de otra más allá de su código?

pues su firma, de hecho la sobrecarga de funciones o métodos tiene este principio como base.

El compilador acepta que dos funciones tengan el mismo nombre siempre y cuando la firma sea distinta. Ejemplo, una recibe dos parametros de entrada y retorna un valor entero, mientra que la otra recibe un solo parámetro de entrada y devuelve un arreglo de enteros.

Lo que sucede es que al compilador le da igual el nombre, ya que en tiempo de ejecución cuando nuestro programa se carga en memoria, las funciones se colocan en el STACK y se hace referencia a ellas mediante una dirección de memoria, no mediante un nombre.

Así que al establecer una firma como un tipo de dato por lógica pudimos haber hecho algo así en el ejemplo anterior:

let closure: (Int) -> Int = {
    
    (value: Int) -> Int in
    
    return value
    
}

print(closure(50))

Pero claro, sería algo bien redundante porque el estar asociando el closure con la constante el compilador de Swift ya puede deducir la firma del mismo. Algo más coherente pudiera ser algo así:

var closure: (Int) -> Int

closure = {
    
    (value: Int) -> Int in
    
    return value
    
}

print(closure(50))

definimos el closure, y luego en otra área del programa podemos asociar un closure, que puede ser cualquiera, siempre y cuando cumpla con la firma que hemos definido. Para que se entienda mejor:

var arithmeticOperation: (Double, Double) -> Double

arithmeticOperation = {
    
    (lNumber: Double, rNumber: Double) -> Double in
    
    return lNumber + rNumber
    
}

print("5 + 5.2 = \(arithmeticOperation(5, 5.2))")

arithmeticOperation = {
    
    (lNumber: Double, rNumber: Double) -> Double in
    
    return lNumber * rNumber
    
}

print("8 * 16 = \(arithmeticOperation(8, 16))")

la salida de este código sería:

5 + 5.2 = 10.2
8 * 16 = 128.0

En este último ejemplo lo primero que hemos hecho es asociar con arithmeticOperation un closure donde sumamos los dos números que pasamos como parámetro.

Luego de esto pasamos a nuestra instancia arithmeticOperation un nuevo closure donde multiplicamos los dos parámetros.

Parámetros y Valores de Retorno

Si podemos almacenar un closure dentro de una variable o una constante, también podemos pasarlo como parámetro de una función o como valor de retorno, ¿cierto?

func runClosure(_ closure: () -> Void) {
    
    closure()
    
} // runClosure

Sí, es correcto, como en este código donde tenemos una función de ejemplo llamada runClosure, la cual recibe un closure como parámetro, un closure de tipo:

() -> Void

Como podemos ver en el ejemplo esta función lo único que hace es ejecutar el closure, nada más. Un closure compatible con esta función no recibe parámetros y no devuelva nada. Un ejemplo válido pudiera ser el siguiente:

let greetingMessage = {
    
    () -> Void in
    
    print("Hola!")
    
} // greetingMessage

runClosure(greetingMessage)

la salida en pantalla ya saben cual es, exacto:

Hola!

En el caso de una función que retorna un closure es similar:

func returnClosure() -> () -> Void {
    
    return {
        
        () -> Void in
        
        print("Hola!")
        
    } // return
    
} // returnClosure

let closure = returnClosure()

closure()

Lógicamente estos son ejemplos bobos, es solo para que vean como funciona el tema de los parámetros y los valores de retorno. La lógica tras estas posibilidades es la siguiente:

  • Closure como parámetros: una función que ejecuta ciertas instrucciones, pero parte de estas instrucciones dependen del usuario de la función, no solo a nivel de parámetros, también a nivel de código. Así que cuando pasamos un closure como parámetro a una función, dicha función delega ese segmento de código al ambito del usuario de la función.
  • Closure como valor de retorno: una función que ejecuta ciertas instrucciones y que finaliza con la conformación de un closure cuyo código está enlazado al código interno de esta función, usualmente capturando algun valor que luego será usado al momento de ejecutar el closure.

Capturando valores

Una de las características más avanzadas de un closure es su habilidad para capturar valores, de hecho aquí es donde muchos se sienten perdidos o no entienden del todo.

Cuando digo capturar valores me refiero a las variables o constantes de su entorno. Por ejemplo:

func travel() -> (String) -> Void {
    
    var counter = 0
    
    return {
        
        (destination: String) -> Void in
        
        counter += 1
        
        print("Has viajado \(counter) \(counter > 1 ? "veces" : "vez") a \(destination)")
        
    } // return
    
} // returnClosure

let spainTravel = travel()

spainTravel("España")
spainTravel("España")
spainTravel("España")

En este ejemplo tenemos una función donde declaramos una variable de nombre counter, la igualamos a cero, y retornamos un closure dentro del cual incrementamos esta variable en uno. La pregunta que surge de aquí es la siguiente:

¿Cómo podemos acceder a la variable counter desde el closure, si al momento de la ejecución (del closure) el ambito de la función travel ya no es accesible?

la pregunta toma más importancia cuando vemos la salida del código anterior:

Has viajado 1 vez a España
Has viajado 2 veces a España
Has viajado 3 veces a España

Para entender lo que sucede hay que dejar algo claro, cuando se ejecuta la línea:

let spainTravel = travel()

la función travel retorna el closure a la constante spainTravel, pero el closure no se ha ejecutado áun, el códido del closure reside dentro de spainTravel en espera a que lo ejecutemos. Si en este punto comentaramos todo el código restante jamás veriamos una salida en pantalla.

El closure se ejecuta por primer vez en la siguiente línea:

spainTravel("España")

donde le pasamos el valor de entrada, en este caso «España» y este nos imprime en pantalla:

Has viajado 1 vez a España

Este es el órden de ejecución, y el comportamiento del closure. Siguiendo estos pasos es lógico que muchos se hagan la pregunta anterior.

Sí la función travel ha terminado su ejecución y el closure se encuentra «detenido en el tiempo» dentro de la constante spainTravel, es lógico que mucho no entiendan como luego podemos acceder a la variable counter que se declara dentro del ambito de la función travel, ambito que ya no es accesible.

Esto es posible debido a que los closures, así como las funciones anidadas, toman las variables o constantes de su entorno como referencias, esto permite que luego de terminada la ejecución de la función travel aún podamos acceder a la información almacenada en las misma.

Los closures son tipos por referencia

En el ejemplo anterior la constante spainTravel hace referencia a un closure que incrementa el valor de counter en 1 por cada viaje, y como los closures son tipos por referencia pues la variable counter tambien se captura por referencia. Es decir que no se crea una copia nueva cada vez que ejecutamos:

spainTravel("España")

se hace referencia a una única versión de la misma, por esta razón es que la variable counter puede incrmenetar su valor en una unidad, de lo contrario siempre sería igual a 1.

Esto sería lo que en C++ se conoce como un puntero a función, un puntero al área de memoria donde se encuentra la función o closure en el caso de Swift.

Dicho eso ¿qué sucedería si hiciéramos esto?

func makeIncrementer(forIncrement amount: Int) -> () -> Int {
    
    var runningTotal = 0
    
    func incrementer() -> Int {
        
        runningTotal += amount
        
        return runningTotal
        
    } // incrementer
    
    return incrementer
    
} // makeIncrementer

let incrementByTen = makeIncrementer(forIncrement: 10)

print(incrementByTen())
print(incrementByTen())
print(incrementByTen())

let alsoIncrementByTen = incrementByTen

print(alsoIncrementByTen())
print(alsoIncrementByTen())
print(incrementByTen())

la salida en pantalla es:

10
20
30
40
50
60

imagino que ya lo supondrán, la respuesta se encuentra en esta línea:

let alsoIncrementByTen = incrementByTen

en la cual igualamos una constante con otra, igualdad que concluye con el paso de una referencia hacia alsoIncrementByTen, referencia evidentemente común con incrementByTen.

En otras palabras: ambas constantes son referencias a una misma posición de memoria, donde se encuentra el closure junto a la variable capturada runningTotal.

Por este motivo, cuando ejecutámos alsoIncrementByTen estamos actuando sobre la misma variable runningTotal, básicamente porque ejecutando el código asoiado al segmento de memoria inicializado por incrementByTen y ahora referenciado también por alsoIncrementByTen.

En caso de haber sido necesaria una copia nueva y completamente aparte a incrementByTen pudiéramos haber sustituido la línea 25 por la siguiente:

let alsoIncrementByTen = makeIncrementer(forIncrement: 10)

quedando un código final:

func makeIncrementer(forIncrement amount: Int) -> () -> Int {

    var runningTotal = 0

    func incrementer() -> Int {

        runningTotal += amount

        return runningTotal

    } // incrementer

    return incrementer

} // makeIncrementer

let incrementByTen = makeIncrementer(forIncrement: 10)

print(incrementByTen())
print(incrementByTen())
print(incrementByTen())

let alsoIncrementByTen = makeIncrementer(forIncrement: 10)

print(alsoIncrementByTen())
print(alsoIncrementByTen())
print(incrementByTen())

y una salida como la siguiente:

10
20
30
10
20
40

Se entiende perfectamente ¿cierto? cualquier comentario o duda, dejadla en los comentarios.

El atributo @escaping

Acabamos de aprender sobre la captura de valores, un característica de los closures bien útil, pero esto no es todo, hay matices que aún debemos conocer, continuemos.

El comportamiento por defecto de un closure consiste en lo que hemos visto hasta ahora, cuando pasamos un closure a una función este se ejecuta dentro del ambito de la misma, cumple su objetivo y poco más. El ciclo de vida por defecto es el siguiente:

  1. Pasamos el closure como parámetro de una función.
  2. La función ejecuta el closure.
  3. La función finaliza su ejecución.

El atributo @escaping se aplica de manera explicita, solo tenemos que anteponer al tipo de dato del closure el atributo «@escaping», de esta manera:

func someFunction(closure: @escaping (Int) -> Void) {
    
    // Código
    
}

El comportamiento de un closure marcado con este atributo se caracteriza por algo que su propio nombre ya nos advierte, un closure @escaping puede «escapar» del ambito de la función. El ciclo de vida de un closure @escaping es el siguiente:

  1. Pasamos el closure como parámetro de una función.
  2. La función ejecuta el closure de manera asincrona o lo almacena fuera del ambito de la función.
  3. La función finaliza su ejecución.
  4. En caso de haber almacenado el closure, pudieramos ejecutarlo en este punto, ya finalizada su ejecución.

Veamos un ejemplo bien sencillo:

var closureStorage: ((Int) -> Void)?

func someFunction(closure: @escaping (Int) -> Void) {

    closureStorage = closure
    
} // someFunction

someFunction { (number) in
    
    print("El valor de la variable number es \(number)")
    
} // closure

closureStorage?(50)

print("¡Punto de referencia!")

En este ejemplo hemos pasado un closure como parámetro de la función someFunction, dentro de esta almacenamos el closure en la variable closureStorage.

Luego de finalizada la ejecución de la función y pasado el closure imprimimos un mensaje de referencia, acto seguido ejecutamos el closure que tenemos almacenado en closureStorage.

La salida en pantalla sería:

El valor de la variable number es 50
¡Punto de referencia!

Ahora veamos un ejemplo similar pero ejecutando el closure de manera asíncrona:

func someFunction(closure: @escaping (Int) -> Void) {
    
    var number = 20
    
    // Ejecutamos el closure luego de 2 segundos apartir
    // de la ejecución de la función
    DispatchQueue.main.asyncAfter(deadline: .now() + 2) {
        
        closure(number)
        
    }
    
} // someFunction

someFunction { (number) in

    print("El valor de la variable number es \(number)")

} // closure

print("¡Punto de referencia!")

la salida en pantalla:

¡Punto de referencia!
El valor de la variable number es 20

Se imprime el mensaje de referencia y la última línea se muestra 2 segundos en el futuro.

Creo que ha quedado claro, cada vez que necesitemos que un closure pasado como parámetro sea almacenado fuera del ambito de la función (o método) lo establecemos como @escaping, o algo mucho más común:

Sí necesitamos hacer una consulta a una API sin que la ejecución de la aplicación se bloquee en espera de la respuesta.

nuevamente, nos apoyamos en el atributo @escaping a favor de ejecutar el closure de manera asíncrona.

Formas de un Closure

Luego de todo lo que hemos visto creo que todos estaremos de acuerdo en los closures son muy flexibles, y así es pero aún hay más, aunque cuentan con una sintaxis básica esta puede simplificarse para volverse mucho más cómoda de usar y optimizar el trabajo.

Veamos esto através de ejemplos, comenzaremos tomando como referencia incicial el siguiente closure:

func calculate(expression: () -> Int) -> Int {
    
    return expression()
    
} // calculate

let result = calculate(expression: {
    
    () -> Int in
    
    5 + 5
    
}) // closure

print("La suma de 5 + 5 es igual a \(result)")

En este ejemplo bien trivial (y absurdo) estamos aplicando la forma básica de un closure, pero podemos reducir el código y ganar en tiempo:

func calculate(expression: () -> Int) -> Int {
    
    return expression()
    
} // calculate

let result = calculate { () -> Int in
    
    5 + 5
    
} // closure

print("La suma de 5 + 5 es igual a \(result)")

En la llamada a la función calculate hemos eliminado los paréntesis y el nombre del parámetro expression. Todo esto es deducido por el compilador.

La función no es necesaria así que podemos eliminarla y de paso simplificar un poco más el closure:

let FivePlusFive = { 5 + 5 }

print("La suma de 5 + 5 es igual a \(FivePlusFive())")

El closure ahora ha quedado reducido a { 5 + 5 } el cual continua teniendo la misma firma: () -> Int. Pero ¿por qué no eliminamos la constante?

print("La suma de 5 + 5 es igual a \({ 5 + 5 })")

Pues lo hacemos y también es válido, aunque no del todo, la salida en pantalla de este print sería:

La suma de 5 + 5 es igual a (Function)

Con este (Function) Swift nos informa de que estamos intentando imprimir un closure en lugar de un valor. Lo que sucede es que un closure debe ser llamado para que su código se ejecute, tal y como sucede con una función.

La solución a nuestro ejemplo más simplifcado es añadir paréntesis al final del closure, veamos:

print("La suma de 5 + 5 es igual a \({ 5 + 5 }())")

estos paréntesis lo que hacen es ejecutar el código del closure, por ende a la función print ahora le llega el valor de retorno en lugar del closure como tal.

¿Cuándo usar un closure?

Esta pregunta es bien frecuente y parte de la respuesta es que los utilizamos de manera obligatoria cuando interactuamos con el propio lenguaje Swift y con muchos de los frameworks de Apple o de terceros.

La otra parte de la respuesta reside en nosotros, una vez que dominemos los closures los podremos incluir en nuestras propias estructuras o clases, en todo lugar donde un closure sea la solución más optima y elegante.

Para no dejar la respuesta aquí, veamos algunos ejemplos al mismo tiempo que seguimos aprendiendo.

El método sort

Este método forma parte del lenguaje Swift, es una función de órden superior y recibe un closure como parámetro.

Imaginemos que somos los organizadores de una comunidad que cuenta con varias organizaciones y queremos mantener un registro de cuantos voluntarios tenemos por cada organización. Para comenzar a trabajar sobre esto hemos creado un arreglo donde almacenamos toda esta información:

var volunteerCounts = [1, 3, 40, 32, 2, 53, 77, 13]

hemos introducido la cantidad de voluntarios en la medida que nos han dado la información, por lo que el arreglo lo tenemos completamente desorganizado, sería genial que lo pudiéramos tener de menor a mayor.

Aquí llegan el método sort que nos permite organizar nuestro arreglo. Tomando como argumento un closure donde describimos como se debe organizar el Array.

El closure toma dos argumentos cuyos tipos tienen que ser iguales al tipo de dato de cada elemento en el arreglo y debe de retornar un valor booleano.

Estos dos argumentos son comparados para generar el valor de retorno, lo que representa si la instancia en el primer argumento debe de ser organizada ante de la instancia del segundo argumento.

Para esto nos valemos del operador < en pos de lograr un orden ascendente o descendente en caso de usar >. El método sort termina por retornar un nuevo Array pero ya organizado tal  y como hayamos especificado en el closure.

Veamos un ejemplo:

var volunteerCounts = [1, 3, 40, 32, 2, 53, 77, 13]

volunteerCounts.sort(by: {
    
    (firtElement: Int, secondElement: Int) -> Bool in
    
    return firtElement < secondElement
    
})

print(volunteerCounts)

la salida en pantalla:

[1, 2, 3, 13, 32, 40, 53, 77]

Como podemos ver aquí hemos utilizado un closure, en este caso para interactuar con una función propia del lenguaje.

Este código también se puede implementar sin utilizar closures, pero desde mi punto de vista de esta manera el código luce mucho más limpio y elegante pero aún así tenemos demasiadas líneas, aquí una nueva versión:

var volunteerCounts = [1, 3, 40, 32, 2, 53, 77, 13]

volunteerCounts.sort(by: { firtElement, secondElement -> Bool in firtElement < secondElement })

print(volunteerCounts)

Como pueden observar hemos reducido nuestra expresión a una sola línea, pero aún podemos ir un poco más allá:

var volunteerCounts = [1, 3, 40, 32, 2, 53, 77, 13]

volunteerCounts.sort(by: { $0 < $1 })

print(volunteerCounts)

Aquí no acaba todo, esta versión del código puede sufrir aún más cambios y es que como el método sort solamente recibe un parámetro, un closure  en este caso, pues el compilador de Swift, que es todo un maestro infiriendo tipos y sintaxis, también nos permite hacer lo siguiente:

var volunteerCounts = [1, 3, 40, 32, 2, 53, 77, 13]

volunteerCounts.sort { $0 < $1 }

print(volunteerCounts)

si este cambio no te parece mucho pues aquí tienes la última versión más simplificada:

var volunteerCounts = [1, 3, 40, 32, 2, 53, 77, 13]

volunteerCounts.sort(by: < )

print(volunteerCounts)

Como vemos es una versión mucho más compacta que el resto, pero también más críptica y puede que para personas con poca experiencia pueda incluso hasta lucir ilegible.

La versión que implementemos ya depende de nosotros, Swift como lenguaje nos permite ser tan flexible como estos ejemplos demuestran y creo que resulta suficiente para cualquiera que sea nuestra necesidad.

El método map

Ahora veamos otro ejemplo, uno muy similar, me refiero al método map que también miembro del tipo de dato Array y con el cual tenemos que interactuar haciendo uso de los closures. Analicemos su comportamiento:

let arrayOfNumbers = [10, 20, 30, 40, 50]

let addOneToEveryNumber = arrayOfNumbers.map { number in number + 1 }

print(addOneToEveryNumber)

la salida en pantalla:

[11, 21, 31, 41, 51]

En este ejemplo el método map aplica el closure a cada elemento en el Array tal y como ocurre con el método sort.

Un dato curioso y bastante util es que el método map nos permite devolver un valor de retorno distinto al tipo de dato correspondiente al arreglo.

Si presionamos Control + Espacio mientras el cursor se encuentra en la palabra map obtendremos una ayuda rápida que nos informa sobre que parámetros recibe este método y que tipo de dato retorna, en este caso la ayuda nos indica:

func map<T>(_ transform: (Element) throws -> T) rethrows -> [T]

aquí podemos ver como al inicio se nos informa que el método devolverá un arreglo de tipo T y que recibe como parámetro un closure que toma como argumento un entero y devuelve un valor de tipo T.

Imagino que si eres muy nuevo en el tema de la programación te preguntarás que significa T, throws o rethrows. Una respuesta más profunda nos redirigiría hacia el tema de los genéricos y la gestión de errores, temás un poco más avanzados que veremos en otros artículos.

Por el momento la T viene siendo un placeholder para cualquier otro tipo de dato, la T se puede convertir en un Int, String Double, etc.

Veamos a continuación el método map dentro de un ejemplo un poco más complejo:

let digitNames = [
    
    0: "Zero", 1: "One", 2: "Two",   3: "Three", 4: "Four",
    5: "Five", 6: "Six", 7: "Seven", 8: "Eight", 9: "Nine"
    
]

let arrayOfNumbers = [10, 20, 30, 40, 50]

print("Arreglo inicial: \(arrayOfNumbers)")

let convertToTheStringEquivalent = arrayOfNumbers.map {
    
    number -> String in
    
    var output = ""
    
    var tempNumber = number
    
    while tempNumber > 0 {
        
        output = digitNames[tempNumber % 10]! + output
        
        tempNumber /= 10
        
    } // while
    
    return output
    
}

print("Arreglo final: \(convertToTheStringEquivalent)")

la salida en pantalla:

Arreglo inicial: [10, 20, 30, 40, 50]
Arreglo final: ["OneZero", "TwoZero", "ThreeZero", "FourZero", "FiveZero"]

en este ejemplo el funcionamiento es similar, a cada elemento del arreglo se le aplica el closure implementado y se retorna al final un nuevo arreglo. Pero esta vez de tipo String con el equivalente en texto de cada dígito, como podemos observar en la salida en pantalla.

Bloques de Finalización

Una de esas ocasiones donde podemos tener la seguridad de que el uso de un closure es una buena elección es en aquellas donde necesitamos ser notificados de que cierta tarea ha finalizado.

Cuando utilizamos un closure en una ocasión similar se le denomina como bloque de finalización o Completion Bloks en inglés. Veamos un ejemplo trivial:

class Pokemon: CustomStringConvertible {
    
    var name: String
    
    init(name: String) {
        
        self.name = name
        
    } // init
    
    var description: String { return "Pokemon \(name)" }
    
} // Pokemon

func evolve(seconds: Int, pokemon: Pokemon, closure: @escaping ()->()) -> String {
    
    let time = DispatchTime.now() + .seconds(seconds)
    
    DispatchQueue.main.asyncAfter(deadline: time) {
        
        pokemon.name = "Raichu"
        
        closure()
        
    }
    
    return "A \(pokemon.name) le tomará \(seconds) segundo para evolucionar."
    
} // evolve

let myPokemon = Pokemon(name: "Pikachu")

print("Antes de evolucionar: \(myPokemon)")

let logMessage = evolve(seconds: 3, pokemon: myPokemon) {
    
    print("Proceso de Evolución Finalizado: Pikachu ha evolucionado a \(myPokemon)")
    
}

print(logMessage)

print("Despues de la llamada a la función: \(myPokemon)")

print()

print("...a solo 3 segundos desde la llamada a la función el bloque de finalización (closure) se ejecutará.")

print()

La salida en pantalla sería:

Antes de evolucionar: Pokemon Pikachu
A Pikachu le tomará 3 segundo para evolucionar.
Despues de la llamada a la función: Pokemon Pikachu

...a solo 3 segundos desde la llamada a la función el bloque de finalización (closure) se ejecutará.

Proceso de Evolución Finalizado: Pikachu ha evolucionado a Pokemon Raichu

En este ejemplo tenemos una clase y una función, la clase Pokemon que representa a un pokemon cualquiera y la función evolve (evolucionar) que se encarga de evolucionar en este caso solamente a Pikachu.

Teniendo en cuenta que cuando evolucionamos un Pokemon deseamos seguir jugando, este proceso no puede bloquear la ejecución de nuestro juego.

Por eso hemos implementado una operación concurrente mediante la cual pasamos la ejecución de este bloque al fondo. Devolvemos un mensaje informando de cuanto va a tomar la operación y continuamos con la ejecución de nuestro código.

La función evolve recibe tres parámetros, el primero es el tiempo que tomará el proceso de evolución, el segundo es una instancia de la clase Pokemon y el último es un closure marcado como @escaping ya que este se ejecutará de manera concurrente luego que la función evolve haya terminado su ejecución.

Nota: En futuros artículos hablaremos sobre Grand Central Dispatch (GCD), la API de bajo nivel que nos permite realizar operaciones de concurrencia en Swift.

Lo curioso de este ejemplo está en la salida en pantalla, en la última línea donde somos notificados al momento de finalizar la ejecución del closure.

En este caso solo hemos diferido la ejecución, pero bien puede haber sido una operación de copia, una descarga de datos, un sistema que detecte cuando hemos perdido la conexión con cierto servidor y que verifique la conectividad de manera automática, etc.

En todos estos ejemplos y en muchos más es bien util poder reducir el impacto de ciertos eventos al usuario final. Un bloque de finalización nos puede servir como una especie de evento que se dispara al terminar una operación y con esto evitamos bloquear la interfaz de usuario.

Creación de Instancias

En ocaciones es conveniente hacer uso de un closure para declarar ciertas instancias, ya sean constantes o variables. Al mismo tiempo que mantenemos ciertas configuraciones iniciales de la instancia dentro de las llaves del closure.

Un segmento que bajo otro enfoque iría en el inicializador de la clase / estructura o en el método viewDidLoad() de algun ViewController.

Veamos a que me refiero:

let nameLabel: UILabel = {
    
    let label = UILabel()
    
    label.text = "Name:"

    label.textAlignment = .left
    
    label.numberOfLines = 1
    
    label.font = UIFont.monospacedDigitSystemFont(ofSize: 20, weight: UIFont.Weight.regular)

    label.translatesAutoresizingMaskIntoConstraints = false
    
    return label
    
}() // nameLabel

Este ejemplo es código Swift pero ya estamos en el área de iOS, hemos definido una constante de nombre nameLabel y de tipo UILabel.

Esta constante es incializada con un closure, dentro de él establecemos un hambito desde el cual procedemos a crear la instanacia que al final retornamos, quedando así nameLabel igual a la instancia de UILabel que establecemos dentro del closure, llamada label.

Dentro del closure creamos la constante de nombre label, la inicializamos para posteriormente configurarla con los parámetros necesarios antes de añadirla a nuestro view. Retornamos la instancia de label y finaliza el closure cerrando el bloque con la llave final y especificando los paréntesis.

La declaración de estos paréntesis finales es mandatory (obligatoria), si no lo hacemos obtendremos un error:

error: cannot convert value of type '() -> _' to specified type 'UILabel'

donde básicamente se nos informa de que no se puede convertir el closure de tipo «() -> _» al tipo definido por nosotros, en este caso de tipo UILabel.

Lo que sucede aquí es lo siguiente: como pueden observar no estamos declarando el cabezal del closure, el clásico:

{ (parámetros-de-entrada) -> tipo_de_retorno in

   declaraciones

}

que es al final lo que establece el tipo de closure. Como esto no lo estamos declarando el compildor de Swift tiene que intuir el tipo del mismo, de ahí que el propio error nos comenta que es de tipo:

() -> _

Es decir que es puro código, no recibe parámetros y tampoco devuelve ningún valor. Algo que lógicamente no es lo que deseamos, y aquí es donde entran los paréntesis finales.

Recordemos que los closures son funciones anónimas, y como en toda función cuando vamos a ejecutarla agregamos los parentesis que corresponden a los parámetros de entrada.

En nuestro caso sucede lo mismo, estos paréntesis lo que hacen es ejecutar el código dentro del closure, y como dentro de este retornamos la instancia label que es de tipo UILabel. Pues de ahí es que el compilador infiere el tipo de dato y ahora ambos son de tipo UILabel.

Dicho de otra manera: cuando especificamos los dos paréntesis estamos informando al compilador de que no deseamos igualar la constante nameLabel al closure como tal y sí a la instancia que este devuelva.

Programación Funcional

Cuando adoptamos un enfoque funcional para nuestro código el uso de closures es bien frecuente, sobre todo cuando estamos interactuando con funciones de orden superior (o higher order functions en Inglés).

Veamos un ejemplo sencillo:

let array = [1, 2, 8, 35, 14, 52, 17, 23]

let smallerThanTwenty = array.filter { $0 < 20 && $0 > 5 }

print(smallerThanTwenty)

En este ejemplo hacemos uso de la función filter la cual nos permite a través de un closure filtrar rangos como hemos hecho en este ejemplo, la salida en pantalla sería:

[8, 14, 17]

De hecho muchos de nosotros hemos estado usando funciones de orden superior quizás sin darnos cuenta, estas se utilizan con cierta frecuencia en Cocoa Touch.

En clases como UIViewController, URLSession o UIView podemos encontrar ejemplos de métodos que toman closures como argumentos, dígase:

viewController.present(anotherViewController, animated: true) {
    
    // Esto es un closure
    
} // present

UIView.animate(withDuration: 3) {
    
    // Esto es un closure
    
} // animate

Los closures, como toda característica que un lenguaje nos brinda, siempre hay que usarlos teniendo en cuenta su naturaleza. Recordando que:

  • Nos permiten capturar valores y hacer uso de estos, incluso cuando el ámbito de la función que los definió ya no existe.
  • Que su sintaxis es muy flexible y en ocasiones nos pueden ayudar con la limpieza y mejorar así la legibilidad del código.
  • Los bloques de finalización, que son una herramienta bien importante cuando estamos frente a operaciones de alto impacto en la experiencia de usuario.
  • Las funciones de orden superior.

Teniendo en cuenta estos factores no será mucho más fácil determinar si una ocasión amerita el uso de un closure o no. La realidad es que la propia necesidad nos invocará a este análisis y con el tiempo, más la practica del día a día, los closures se convertiran en una herramienta más dentro de las tantas posibilidades que nos brinda el lenguaje.

Conclusiones

Como han podido comprobar, el conocer los closures nos permite movernos con más soltura, ser más creativos y flexibles. Hemos constatado como hay librerías del propio lenguaje que implementan closures como argumentos de sus métodos, esto nos obliga a dominarlos y sentirnos cómodos con ellos.

Por todo siempre esto los invito a que practiquen mucho, en general y no solamente sobre esté tópico.

Alguien que no domine un lenguaje en su totalidad no cuenta con las herramientas necesarias para brindar un producto final de alta calidad.

Su creatividad en cuanto a la aplicación como un todo, siempre se verá limitada a lo que conoce, cuando quizás ese aspecto que no domina sería la pincelada necesaria para que su proyecto logre sobresalir.

Falta aún mucho por aprender en nuestro camino a convertirnos en iOS Developer. Suscríbete a nuestra lista de correo y síguenos en nuestras redes sociales. Mantente al tanto de todas nuestras publicaciones.

Espero que todo cuanto se ha dicho aquí, de una forma u otra le haya servido de aprendizaje, de referencia, que haya valido su preciado tiempo.

Este artículo, al igual que el resto, será revisado con cierta frecuencia en pos de mantener un contenido de calidad y actualizado.

¡Cualquier duda o sugerencia, ya sea errores a corregir o ejemplos a añadir, será más que bienvenida, necesaria!

Deja una respuesta

Su dirección de correo electrónico no será publicada.

Este sitio usa Akismet para reducir el spam. Aprende cómo se procesan los datos de tus comentarios.

Este sitio web utiliza cookies para mejorar su experiencia. Asumiremos que está de acuerdo con esto, pero puede optar por no participar si lo desea. Aceptar Leer Más

RECIBE CONTENIDO SIMILAR EN TU CORREO

RECIBE CONTENIDO SIMILAR EN TU CORREO

Suscríbete a nuestra lista de correo y mantente actualizado con las nuevas publicaciones.

Se ha suscrito correctamente!