¿Qué es un Closure?

Tutorial Swift – ¿Qué es un Closure?

En este Tutorial Swift aprenderemos sobre los closures, bloques autónomos funcionales que podemos utilizar en nuestro código. En Swift estos serían como las funciones anónimas de toda la vida, similares a los bloques en C o Objective-C y a los lambdas en otros lenguajes de programación, por lo que no nos debe de resultar muy difícil su dominio, de hecho ya los hemos usado, ya que las funciones son consideradas un tipos especial de closures.

Las expresiones closures en Swift se diferencian de las funciones en que no tienen nombre y en que podemos crear bloques de código de una manera bastante simple sin tener que generar una declaración completa como en el caso de las funciones donde todo esto si es más estricto, esta flexibilidad nos permite, de una manera mucho más fluida, pasar los closures como parámetro o como tipo de retorno.

Sintaxis

Los closures tienen un estilo muy bien definido, optimizaciones que fomentan limpieza y claridad con un enfoque en la legibilidad en cualquiera de los escenarios donde estas se puedan encontrar. La sintaxis básica de un closure no es para nada compleja ni enrevesada, veamos un ejemplo de la forma general que adoptan:

…como ven apartando las llaves y la ausencia de un nombre el resto es bastante parecido a la definición de una función.

El método sort

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 por lo que hemos creado un arreglo donde almacenamos toda esta información:

…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 las buenas noticias, sí pues la librería estándar de Swift nos ofrece el método sort(_:) que nos permite especificar como organizaremos nuestro Array.

El método sort(_:) toma un argumento: un closure que describe como se debe de 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 < (menor que) en pos de lograr un orden ascendente o descendente en caso de usar > (mayor que). El método sort(_:) termina por retornar un nuevo Array pero ya organizado tal  y como hayamos especificado en el closure.

Veamos un ejemplo:

…en este ejemplo hemos creado una función llamada sortAscending la cual toma dos argumentos, en este caso dos enteros del array y analiza cual es menor que el otro, en caso del primer elemento ser menor devuelve True como valor booleano de retorno y con esto le informa al método sort(_:) que este elemento debe ir primero que el segundo en el nuevo array que se está conformando y que quedaría de la siguiente forma:

En caso de que se lo estén preguntando, hemos usado la función sortAscending ya que todas las funciones son closures con nombres, de hecho como ven en la llamada, los parámetros son omitidos por nosotros y dejamos esta tarea el método sort(_:) que es el encargado de esta tarea, y todo es posible ya que se le está dando un trato de closure a esta función, de lo contrario y en otro contexto el compilador nos obligaría a que especificar los argumentos.

Ahora, en lugar de declarar una función aparte hagamos uso de una expresión closure y optimicemos el código del ejemplo anterior:

…esta refactorización nos deja con un código final mucho más limpio y elegante pero aún así tenemos demasiadas líneas de código, aquí una nueva versión:

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

…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:

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

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 un ejemplo similar, pero donde haremos uso del método map(_:) también miembro del tipo de dato Array y con el cual tenemos que interactuar haciendo uso de los closures. Analicemos su comportamiento:

…otra versión de este código pudiera ser:

…la salida en pantalla de ambos códigos sería la misma:

Como podemos constatar en este ejemplo, el método map(_:) aplica el closure que hemos implementado a cada elemento en el Array, pero el método map también nos permite retornar un valor de retorno distinto al tipo de dato del parámetro de entrada. Si presionamos Control + Espacio mientras el cursor se encuentra en la palabra map obtendremos una ayuda rápida que nos informa que parámetros recibe este método y que tipo de dato retorna, en este caso la ayuda nos informa que:

…aquí podemos ver como al inicio se nos informa que el método devolverá un arreglo de T tipo de dato y que recibe como parámetro un closure / función que toma como argumento un entero y devuelve un valor de tipo T y T evidentemente puede ser Int, String Double, etc. Las palabras claves throws y rethrows las veremos en próximas entregas de este Curso Swift así que por ahora solamente ignórenlas.

Veamos a continuación el código anterior con unas pocas modificaciones:

…la salida en pantalla de este código es:

…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.

Capturando valores

Un closure puede capturar tanto constantes como variables del entorno que lo rodea y, con capturar, me refiero a que puede acceder a estos y modificar sus valores, incluso cuando el ámbito de estos ya no existe. La forma más simple en la que podemos encontrar un caso así es cuando no encontramos ante funciones anidadas, veamos un ejemplo:

…en este ejemplo la función makeIncrementer toma como parámetro un valor Int y retorna una función sin argumentos de entrada y que retorna un valor Int. Hasta aquí todo bien, ahora comienza lo interesante y es en la línea 3 donde definimos la variable runningTotal la cual almacena el valor total que vamos usando como base cada vez que hacemos una llamada a la función que retornamos. La función anidada incrementer como ya comentamos al inicio no toma argumento y devuelve un valor Int, dentro del cuerpo de esta sumamos al valor de runningTotal el incremento especificado cuando se efectuó la llamada a la función makeIncrementer para finalizar retornando este valor en la línea 9, la línea 13 retorna la función anidada acorde al valor de retorno especificado por makeIncrementer. En la línea 17 declaramos la constante llamada incrementByTen la que igualamos al valor retornado por la función makeIncrementer pasándole el valor inicial de 10 definiendo así un incremento de 10 unidades cada vez que hagamos una llamada a la función incrementer ahora almacenada en incrementByTen, en este punto quiero remarcar el hecho de que al finalizar la ejecución de esta línea la función incrementByTen ya no tiene acceso al ámbito de makeIncrementer . Luego en la línea 19 hacemos la primera llamada a la función incrementByTen() obteniendo como resultado el valor de 10 ya que la variable runningTotal fue inicializada con el valor de 0 (cero) y así le siguen dos llamadas más en las líneas siguientes, veamos la salida en pantalla:

…como podemos constatar las siguientes llamadas siguieron aumentando el valor anterior en 10, como es esto posible si ya no podemos acceder al valor de runningTotal?

Esto es posible debido a que las funciones / closure anidados toman las constantes o variables del entorno que las rodea como referencias, es decir una referencia a las mismas, esto permite que luego de terminada la ejecución de makeIncrementer y finalizado el acceso a las variables definidas en su ámbito aún podamos acceder ala información almacenada en las mismas.

Los closures son tipos por referencia

En el ejemplo anterior la constante incrementByTen hace referencia a un closure que incrementa el valor de la variable runningTotal la cual es capturada por referencia, es decir que no se crea una copia nueva, se hace referencia a una única versión de la misma. Pues resulta que los closures comparten este comportamiento ya que son tipos por referencia, esto quiere decir que cada vez que definimos una variable o constante y la igualamos a una función / closure esta se vuelve automáticamente una referencia a esa función / closure. Esto al igual que con las variables o constantes capturadas también quiere decir que en memoria tenemos una copia única del closure (incrementer) y la variable (incrementByTen) es solamente una referencia. Esto en C++ sería un puntero a función, que sería lo mismo que decir un puntero al área de memoria donde se encuentra la función o closure en el caso de Swift. Aparte de la comodidad y flexibilidad que esto nos brinda creo que es más que evidente el hecho de que también nos ayuda con el consumo de memoria.

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

…la línea 25 es en la que me baso para la pregunta. En esta creamos una constante llamada alsoIncrementByTen y la igualamos con la constante incrementByTen que referencia al closure que incrementa la variable runningTotal. Al momento de llegar a esta línea la variable runningTotal es igual a 30 por lo que al ejecutar las línea 27 y 29 deberíamos de tener en pantalla los valores de 40 y 50 y así es, pero ¿cual valor obtendríamos al ejecutar la última línea donde nuevamente ejecutamos incrementByTen? pues el valor resultante sería 60, aquí la salida en pantalla:

…lo que sucede en este caso imagino que ya lo supondrán, y la respuesta se encuentra en esta línea:

…en la cual igualamos una constante con otra y que a su vez es una referencia a un closure, por lo que esta igualdad concluye con el paso de una referencia hacia alsoIncrementByTen, referencia evidentemente común con incrementByTen y que en otras palabras significa que 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ábamos alsoIncrementByTen seguíamos incrementando la misma variable runningTotal que cuando ejecutábamos incrementByTen.

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:

…quedando un código final:

…y una salida como la siguiente:

El atributo @noescape

Cuando declaramos una función que toma un closure como uno de sus argumentos, podemos escribir @noescape antes del nombre del parámetro. Esto nos permite garantizar que el closure no será almacenado en ninguna variable o constante, que no será usado más adelante luego de terminada la ejecución de esa función y que tampoco será usado asincrónicamente. Veamos un ejemplo:

…en este ejemplo hemos declarado un arreglo, dos funciones y una clase. En el arreglo almacenaremos un closure, las dos funciones las utilizo para demostrar la diferencia entre una cuyo argumento cuenta con el atributo @noescape y la otra que no, luego continuamos con una clase con un método desde donde hacemos las llamadas a las funciones antes comentadas, en esta misma clase nos encontramos con un bloque deinit (este bloque sería el equivalente al destructor de C++) donde imprimimos un mensaje al momento de ser liberada la memoria ocupada por la clase, por último un bloque do que lo he usado solamente para generar un ámbito para que la clase sea liberada al finalizar este ya que debido a la naturaleza del propio Playground si no hacemos esto el bloque deinit jamás será ejecutado. Veamos la salida en pantalla de este código:

…tras la impresión de estas líneas nos percatamos que el mensaje:

“SomeClass deinit, goodbye!“

…no ha sido mostrado y esto básicamente es debido a que existe una referencia fuerte a la clase por parte del closure que tenemos almacenado en el arreglo, si descomentamos la línea 67 veremos una nueva salida:

Con lo que acabo de expresar, más los propios comentarios del código creo que es suficiente para terminar de entender el funcionamiento del mismo, de no ser así pues, en los comentarios de este artículo, me pueden dejar sus dudas.

Algo interesante es que cuando declaramos un closure con este atributo y dadas las características antes comentadas, pues el compilador como ya tiene conocimiento del tiempo de vida del mismo optimiza el código de una manera mucho más agresiva.

El atributo @autoclosure

El atributo @autoclosure nos permite optimizar nuestro código al mismo tiempo que nos facilita la vida ya que este lo podemos aplicar a un argumento de una función que recibe un closure, permitiéndonos a la hora de llamar la función pasar una expresión normal sin la necesidad de las llaves. Este atributo envuelve la expresión y crea de manera automática el closure que la función espera recibir.

Haciendo uso del autoclosure también podemos demorar la evaluación del closure ya que la expresión no se ejecuta hasta que efectuemos su llamada de manera explicita. La posibilidad de demorar la evaluación viene a ser muy útil cuando la expresión se conoce que tiene un impacto negativo en el rendimiento de la aplicación, quizás debido a la carga de información desde una base de datos. Tener bajo nuestro control cuando una operación así se lleva a cabo nos flexibiliza a actuar de una manera más estratégica y eficiente, nos permite reducir al máximo las molestias que esto pudiera generar al cliente.

En el código a continuación demostramos como podemos demorar la evaluación de un closure:

…la salida en pantalla:

En la primera línea creamos un arreglo con los 5 nombres de una fila de personas que están esperando por ser atendidas, seguimos imprimiendo la cantidad de elementos en el arreglo, en la tercera línea creamos una constante que va a almacenar un closure,  la expresión asignada a esta constante elimina el primer elemento del arreglo simulando que la persona está siendo atendida, y aquí es cuando ocurre lo curioso del caso ya que se está ejecutando el método removeAtIndex(0) mientras que en la salida en pantalla de la línea 4 podemos constatar de que el número de elementos en el arreglo sigue siendo 5, con esto comprobamos de que el closure no ha sido ejecutado, en la línea 5 imprimimos un mensaje en la pantalla del local donde informamos a cual cliente estamos atendiendo, obtenemos el nombre del cliente a partir de la constante que almacena el closure, llamada que evidentemente evalua la expresión y la ejecuta, momento donde realmente eliminamos el primer elemento del arreglo como podemos comprobar en la última línea donde el número de elementos ahorita es 4.

Explicado esto optimicemos este código y de paso veamos como implementar todo esto haciendo uso de una función:

…este código es más compacto y equivalente al anterior pero hagámosle otro cambio:

En esta versión hemos declarado el argumento de la función con el atributo @autoclosure permitiéndonos al momento de su llamada obviar la declaración del closure de manera explicita, por lo que ahorita podemos interactuar con su argumento como si este tomara un String en lugar de un closure. Como podemos constatar en la última línea escribimos la expresión de manera directa y sin las llaves.

Antes de finalizar analicemos el siguiente ejemplo:

…luego de leer los comentarios en el código y de percatarnos que el compilador nos muestra un error, descubrimos otra característica del atributo @autoclosure y es que este implica @noescape por defecto, es decir que cuando declaramos @autoclosure también se aplican las características del atributo @noescape y esto es lo que justifica el error. Como podemos observar la función collectCustomerProviders() guarda el closure pasado como argumento en el arreglo customerProviders y esto como ya vimos aparte de crear una referencia fuerte desde la función hacia el arreglo, algo que quizás no sea lo deseado, pues también viola el atributo @noescape ya el argumento está siendo almacenado fuera del ámbito de la función.

Pero si aun sabiendo esto queremos hacer uso del atributo @autoclosure por la comodidad que nos brinda…

¿Qué solución tenemos?

Pues Swift haciendo gala nuevamente de su versatilidad nos permite inhabilitar el comportamiento @noescape del atributo @autoclosure y esto lo logramos con su forma @autoclosure(escaping), veamos como quedaría al código anterior ya corregido:

…la salida en pantalla:

Como han podido comprobar, el conocer los closures nos permite movernos con más soltura, ser más creativos y flexibles, aparte de que hemos constatado como hay librerías del propio lenguaje que implementan en sus métodos argumentos en forma de closures, esto nos obliga a dominarlos y sentirnos cómodos con ellos. Por todo esto los invito a que practiquen mucho, 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 arquitectura 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.

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 sugerencia, ya sea errores a corregir, información o ejemplos a añadir será, más que bienvenida, necesaria!