¿Cuándo usar un Closure?

Tutorial Swift – ¿Cuándo usar un Closure?

Josué V. Herrera Swift Avanzado, Tutoriales 0 Comments

En el Tutorial Swift de hoy hablaremos sobre cuando usar un Closure, aprenderemos a identificar esos casos donde los closures son la opción más óptima y legible.

Como algunos sabrán, en este sitio ya contamos con un artículo dedicado al aprendizaje de los closures en el lenguaje Swift, por este motivo se asume su lectura y paso a enfocarme en el objetivo descrito para este artículo.

El uso de una característica siempre está determinado por las posibilidades de ella misma, así 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 en 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, todo esto conforma la guía para saber cuando poder usarlo y mejor aún usarlo de manera correcta. Quiero puntualizar en esta última parte: “de manera correcta”… ya que si es por usar, podemos aplicar un closure en miles de ocasiones pero estaríamos añadiendo complejidad innecesaria a nuestro código, por no hablar de la inquietud o confusión que se generaría cuando otro desarrollador lo leyera e intentara interpretar la necesidad que motivó o justificó algo como esto:

Creo que es más que evidente que para efectuar una suma o cualquier otro cálculo no es necesario crear una función que acepte un closure para ejecutar una operación. Ciertamente pudiéramos simplificar el código anterior a la siguiente versión:

…más sintetizado:

…pero aún así creo innecesario el uso de un closure en este caso y contexto.

Capturando Valores

Los Closures son tipos por referencia y una de las características más importantes de estos es la posibilidad que tienen de capturar valores de su entorno. Esta operación vincula al closure con el valor mediante un enlace fuerte en pos de que este sea mantenido hasta su ejecución, aún cuando el ámbito de estos valores ya no esté disponible.

Veamos que nos brinda esta característica con un ejemplo básico:

La salida en pantalla sería:

En este ejemplo tenemos dos funciones, una de estas de nombre createClosure es la que se encarga, como su nombre indica, de crear el closure con el cual trabajaremos y al mismo tiempo constituye el ámbito donde se encuentra la variable, de nombre name, que capturaremos. En la línea 15 efectuamos la primera llamada, a la función createClosure, e igualamos su resultado en una variable de nombre closure y de tipo () -> (). Luego de esta acción la variable name es capturada, aún cuando la ejecución de la función que la creó y que constituye su ámbito, ya ha finalizado. Por último de la línea 15 a la 21 efectuamos una última llamada, en este caso a la función definida al inicio, de nombre receiveClosure, y le pasamos la variable closure, acto seguido se ejecuta el closure por primera vez y con esto la salida en pantalla. Como se puede constatar el resultado es el correcto y esto sin dudas constituye algo sólido, una pista determinante a la hora de elegir el uso de un closure en nuestro código.

Legibilidad en el Código

Quedándonos con la captura de valores en nuestra mente otro aspecto bastante comentado sobre los closure es el impacto de estos en la legibilidad del código. Pero en mi caso personal encuentro esto bastante relativo ya que lo que para mi quizás puede resultar legible y bastante claro o como se diría en inglés self-explanatory (Autoexplicativo) pues para otra persona quizás no lo sea tanto. Si a esto último le sumamos que algunas veces en pos de la legibilidad creamos código extra, el hecho de perseguir un objetivo, desarrollar un algoritmo, ser óptimo en su implementación, reutilizar código y al mismo tiempo que este sea considerado como legible por todos, pues resulta (en ocasiones) un fin bien complejo de lograr.

Como esto de la legibilidad es así de loco y bastante dado a la interpretación y sobre todo a la manera de analizar de cada uno, aspecto donde influyen bastante los profesores que hemos tenido y su escuela de pensamiento (no filosófico más bien de razonamiento y resolución de problemas). Esto ha dado lugar a que muchas empresas y desarrolladores Indie (independientes) tengan las llamadas Guías de Estilo y Convenciones de Código en donde se tocan temas como el formato de los nombres (usualmente Camelcase), el estilo de programación donde se explica aspectos en detalles como el tamaño máximo de una línea, estilo de indentación, el estilo de las llaves (abiertas al final de la línea o en la siguiente… etc), convención para los comentarios en el código, etc. Estas guías ayudan a disipar lo relativo del tema, conforman el camino hacia ese punto de convergencia en el que todos estamos de acuerdo y donde luego de conocidas las reglas, la refactorización y la lectura de código ajeno en general se vuelve mucho más fluida, legible y clara.

Así que llegado a este punto tiendo a pensar que los closure no son legibles per se, más bien dada su flexibilidad, el minimalismo que podemos lograr y las distintas formas en las que los podemos declarar, esto es lo que realmente hace que puedan ser mucho más legibles que otras soluciones alternativas. Nuevamente, siempre y cuando los usemos en unas formas medianamente entendibles y siempre alejándonos de ese código críptico que usualmente depende más del ego que de razones objetivas o de estilo.

Hasta donde sé Apple no ha publicado alguna convención de código asociada a Swift y por la cual nos podamos guiar, lo más cercano a esto es la Guía de Diseño de API publicada en Swift.org  pero al final bien puedes crearte tu propio estilo o bien guiarte por alguna referencia en el sector, a mi personalmente me gusta mucho la guía de estilo de Ray Wenderlich. Según esta guía el siguiente ejemplo no sería del todo ideal:

…mientras que una versión más legible y clara sería:

Se nota la diferencia? Pues sí, tal y como les comenté la versión mejorada está dada por la flexibilidad que ofrecen los closures en Swift. En este caso se ha cambiado a la sintaxis trailing closures en la primera versión del método animate y en la segunda donde pasamos dos closures se ha optado por la sintaxis “estándar” donde los closures se encuentran dentro de los paréntesis del método. En la segunda versión y específicamente en el segundo closure se añadió el nombre del parámetro (completition) y se eligió la palabra finished en lugar de f. Vemos que en este último caso es evidente el cambio hacia la legibilidad más lo anteriores de estilo y a mi modo de ver, sea autosugestión o no, luce mucho mejor, es un código que puedo leer e interpretar bastante rápido gracias, sobre todo, a los nombres de los parámetros (withAnimation, animations, completitionfinished en el caso del último closure).

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:

En pos de que este código funcione de manera correcta (si estás usando Playground como yo) necesitamos añadir la librería PlaygroundSupport y establecer la ejecución indefinida en nuestra página actual del Playground. Todo esto con la intención de que al llegar al final del código nuestro segmento concurrente sea capaz de ejecutarse e imprimir en la salida en pantalla. El cabezal del proyecto tiene que lucir así:

Luego de esto ya el código anterior funciona con el comportamiento deseado. De más está decir que el ejemplo está enfocado en Pikachu, no toma en cuenta la posibilidad de otros pokemones.

La salida en pantalla sería:

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, pues queda claro que este proceso no puede bloquear la ejecución de nuestro juego, así que de la línea 19 a la 25 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 hemos diferido la ejecución pero bien puede haber sido una operación de copia por ejemplo, una descarga de datos, pudiéramos tener implementado un sistema que detecte cuando hemos perdido la conexión con cierto servidor y que reintente la conexión de manera automática y nos muestre un mensaje diciendo “No Internet Connection” y luego “Internet Connection Established”, quizás también un sistema que vaya sincronizando ciertos datos locales con la nube, etc. En todos estos ejemplos y en muchos más es de mucha utilidad reducir el impacto de ciertos eventos en la experiencia de usuario y un bloque de finalización nos puede servir como una especie de evento que se dispara al terminar una tarea que usualmente demora pero que al mismo tiempo necesitamos estar al tanto de su progreso.

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 que muestre esto de una manera más gráfica:

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:

De hecho muchos de nosotros hemos estado usando funciones de orden superior quizás sin darnos cuenta ya que se utilizan con cierta frecuencia en Cocoa Touch. Algunos ejemplo serían UIViewController, URLSession o UIView, en estos podemos encontrar ejemplos comunes de métodos que toman closures como argumentos. Ejemplo:

Sin mucho más que agregar los exhorto a que sigan aprendiendo sobre las funciones de orden superior en el lenguaje Swift.

Resumiendo

Los closures, como toda característica que un lenguaje nos brinda, tenemos que usarlos siempre 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 así aumentar la legibilidad del código, también los bloques de finalización que son una herramienta bien importante cuando estamos frente a operaciones de alto consumo de tiempo y por último las funciones de orden superior que acabamos de ver. Teniendo en cuenta todo estos factores no será mucho más fácil determinar si una ocasión amerita el uso de un closure o no.

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!