Los lenguajes compatibles con funciones de primera clase le permiten utilizar funciones y métodos como cualquier otro objeto o valor. Puede pasarlos como argumentos, guardarlos en propiedades o devolverlos desde otra función. En otras palabras, el lenguaje trata las funciones como»ciudadanos de primera clase».

Aunque Swift no es el primer lenguaje que admite esta forma de manejar funciones, normalmente es una característica que se ve en lenguajes más dinámicos como JavaScript o Lua. Por lo tanto, combinar el robusto sistema de tipos estáticos de Swift con funciones de primera clase se convierte en una combinación bastante interesante, y puede permitirnos hacer algunas cosas bastante creativas 😀

¡Esta semana, echemos un vistazo a algunas formas diferentes de usar funciones de primera clase en Swift!

Instabug: Resuelva errores, bloqueos y otros problemas mucho más rápido utilizando los rastros detallados de la pila, los registros de red y los eventos de interfaz de usuario que Instabug adjunta automáticamente a cada informe de errores. Utilizado tanto por mí como por miles de equipos de desarrollo de iOS de todo el mundo. Pruébalo gratis e intégrelo con una sola línea de código.

Pasar funciones como argumentos

Comencemos con lo básico. Dado que las funciones se pueden usar como valores, significa que podemos pasarlas como argumentos. Por ejemplo, digamos que queremos agregar una matriz de subviews a una vista. Normalmente, podríamos hacer algo como esto:

let subviews = subviews.forEach { subview in view.addSubview(subview)}

El código anterior funciona, y no tiene nada de malo. Pero si aprovechamos las funciones de primera clase, en realidad podemos reducir su verbosidad bastante.

Lo que podemos hacer es tratar el método addSubview como un cierre de tipo (UIView) -> Void (ya que acepta una vista para agregar y no devuelve nada). Esto coincide perfectamente con el tipo de argumento que acepta forEach (un cierre de tipo (Element) -> Void, y en este caso el tipo Element es UIView). El resultado es que podemos pasar view.addSubview directamente como argumento a nuestra llamada forEach, así:

subviews.forEach(view.addSubview)

¡Eso es genial! 😎 Sin embargo, una cosa a tener en cuenta, es que cuando se utilizan métodos de instancia como cierres como este, se retiene automáticamente la instancia mientras se retiene el cierre. Esto no es un problema en absoluto cuando se pasa una función como un argumento de cierre sin escape como el anterior, pero para escapar cierres es algo que hay que tener en cuenta para evitar ciclos de retención.

Para obtener más información sobre los argumentos de cierre de escape y de no escape y la captura, consulte «Capturar objetos en cierres rápidos».

Pasar inicializadores como argumentos

Lo bueno es que no solo se pueden usar funciones y métodos como funciones de primera clase en Swift, sino que también puede usar inicializadores de esta manera.

Por ejemplo, digamos que tenemos una matriz de imágenes para las que nos gustaría crear vistas de imagen, y que queremos agregar cada una de esas vistas de imagen a una vista de pila. Usando funciones de primera clase podemos lograr todo lo anterior usando una cadena simple de map y forEach:

images.map(UIImageView.init) .forEach(stackView.addArrangedSubview)

Lo que me gusta de estructurar el código de esta manera es que se vuelve muy declarativo. En lugar de bucles for anidados, simplemente estamos declarando cuál queremos que sea el resultado. Por supuesto, hay un equilibrio entre código declarativo, compacto y legibilidad, pero para operaciones simples como las anteriores, creo que aprovechar las funciones de primera clase puede ser súper agradable.

Puede encontrar más ejemplos y casos de uso para pasar inicializadores como cierres en «Inyección simple de dependencias Swift con funciones» y «Viaje en el tiempo en pruebas unitarias Swift».

Crear referencias de métodos de instancia

Profundicemos un poco más en el maravilloso mundo de las funciones de primera clase 😉. Una cosa que me desconcertó durante mucho tiempo fue el hecho de que recibí sugerencias de finalización automática de métodos de instancia cuando quería llamar a un método estático. Intenta escribir UIView. en Xcode para ver a lo que me refiero, obtienes cada método de instancia como una sugerencia 🤔.

Al principio pensé que era un error de Xcode, pero luego decidí investigarlo. Resulta que para cada método de instancia que tiene un tipo, hay un método estático correspondiente que le permite recuperar ese método de instancia como cierre, pasando una instancia como argumento.

Por ejemplo, podemos usar lo siguiente para recuperar una referencia al método removeFromSuperview para una instancia UIView dada:

let closure = UIView.removeFromSuperview(view)

Llamar al cierre anterior sería exactamente lo mismo que llamar a view.removeFromSuperview(), lo cual es interesante, pero ¿es realmente útil? Echemos un vistazo a algunos escenarios en los que el uso de esta función puede conducir a algunos resultados bastante interesantes.

XCTest en Linux

Una forma en que uno de los frameworks de Apple usa esta característica es cuando ejecuta pruebas usando XCTest en Linux. En las propias plataformas de Apple, XCTest funciona utilizando el tiempo de ejecución de Objective-C para buscar todos los métodos de prueba para un caso de prueba dado y luego los ejecuta automáticamente. Sin embargo, en Linux no hay tiempo de ejecución de Objective-C, por lo que requiere que escribamos un poco de repetición para que se ejecuten nuestras pruebas.

Primero, tenemos que declarar un diccionario estático allTests que contiene una asignación entre nuestros nombres de prueba y los métodos reales a ejecutar:

Luego pasamos el diccionario anterior a la función XCTMain para ejecutar nuestras pruebas:

XCTMain()

Bajo el capó, esto está utilizando la característica de poder extraer métodos de instancia utilizando su equivalente estático, lo que nos permite simplemente referirnos a las funciones por nombre en un contexto estático, al tiempo que permite que el marco genere métodos de instancia para ejecutarse. ¡Muy inteligente!

Sin esta característica, habríamos tenido que escribir algo como esto:

Llamar a un método de instancia en cada elemento de una secuencia

Tomemos esta característica para dar un giro nosotros mismos. Al igual que pudimos pasar el método de instancia de otro objeto como argumento a forEach, ¿no sería genial si también pudiéramos pasar un método de instancia que queremos que realice cada elemento de una secuencia?

Por ejemplo, supongamos que tenemos una matriz de subvistas que queremos eliminar de su superview. En lugar de tener que hacer esto:

for view in views { view.removeFromSuperview()}

¿No sería genial si pudiéramos hacer esto en su lugar:

views.forEach(UIView.removeFromSuperview)

La buena noticia es que podemos, todo lo que tenemos que hacer es crear una pequeña extensión en Sequence que acepte uno de estos métodos de instancia con referencia estática. Ya que son funciones que generan una función (Functionception! 😂 ) su tipo siempre será (Type) -> (Input) -> Output, por lo que para nuestra extensión podemos crear una sobrecarga forEach que acepte un cierre de tal tipo:

¡Ahora podemos llamar fácilmente a métodos de instancia en cada miembro de cualquier secuencia! Implementing

Implementar objetivo / acción sin Objetivo-C

Echemos un vistazo a un ejemplo más. En UIKit, el patrón de objetivo / acción es muy común, para todo, desde observar clics de botón hasta responder a gestos. Personalmente, me gusta mucho este patrón, ya que nos permite usar fácilmente un método de instancia como devolución de llamada sin tener que preocuparnos por el problema del ciclo de retención que discutimos anteriormente (al hacer referencia a un método de instancia como cierre).

Sin embargo, la forma en que se implementa target/action en UIKit depende de los selectores Objective-C (por lo que debe anotar los métodos de acción privada con @objc). Digamos que queremos agregar el patrón de objetivo / acción a una de nuestras vistas personalizadas, y digamos que queremos hacerlo sin depender de los selectores Objective-C. Eso puede sonar como mucho trabajo y que hará las cosas terriblemente complicadas, pero gracias a las funciones de primera clase, ¡es bastante simple! start

Comencemos definiendo un Action typealias como una función estática que devuelve un método de instancia para un tipo y una entrada dados:

typealias Action<Type, Input> = (Type) -> (Input) -> Void

A continuación, vamos a crear nuestra vista. Crearemos un ColorPicker que le permita al usuario elegir un color en una aplicación de dibujo y agregar un método para agregarle una acción & de destino. Haremos un seguimiento de todas las observaciones como cierres, y cada vez que se ejecuta un cierre generamos un método de instancia para el objetivo dado y lo ejecutamos, de la siguiente manera:

Lo bueno es que en realidad podemos usar funciones de primera clase aún más arriba. Al usar la API map en Optional, podemos generar el método de instancia y llamarlo de una sola vez, de esta manera:

observations.append { view in target.map(action)?(view)}

Finalmente, usemos nuestra nueva API de destino / acción en un CanvasViewController, que presentará nuestro ColorPicker. Al igual que agregaríamos una acción de destino & a un UIButton o UIGestureRecognizer, simplemente podemos pasar el controlador de vista y un método de instancia para ejecutar, como este:

Escriba objetivos seguros & acciones sin ningún selector de Objective-C ni riesgos de fugas de memoria, utilizando solo unas pocas líneas de código, ¡bastante genial! Support

Apoye Swift de Sundell echando un vistazo a este patrocinador:

Instabug: Resuelva errores, bloqueos y otros problemas mucho más rápido utilizando los rastros detallados de la pila, los registros de red y los eventos de interfaz de usuario que Instabug adjunta automáticamente a cada informe de errores. Utilizado tanto por mí como por miles de equipos de desarrollo de iOS de todo el mundo. Pruébalo gratis e intégrelo con una sola línea de código.

Conclusión

Las funciones de primera clase son una característica muy potente. Al ser capaces de usar funciones y métodos de una manera mucho más dinámica, podemos lograr algunos resultados bastante interesantes, y puede ser realmente útil al implementar ciertos tipos de API.

Sin embargo, en las famosas palabras del tío Ben; con un gran poder viene una gran responsabilidad. Si bien creo que es súper útil aprender sobre este tipo de características y cómo funcionan, también es importante ejercer cierta moderación al usarlas. Nuestro objetivo siempre debe ser crear API que sean agradables y fáciles de usar, y escribir código que sea fácil de leer & mantener. Las funciones de primera clase definitivamente pueden ayudarnos a cumplir ese objetivo, pero si se llevan demasiado lejos, también pueden conducir a todo lo contrario. Como siempre, mi recomendación es experimentar, probar estas características y ver por ti mismo si y cómo se pueden usar en tu propio código.

Deja una respuesta

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