Píldoras de códigoSwift
Tendencia

Swift 5.2, valores llamables de tipos nominales definidos por el usuario (callAsFunction)

El nuevo e interesante patrón de diseño en programación dinámica

Resumen del artículo
  • Aprendemos una nueva característica de Swift 5.2, unida a la programación dinámica de funciones matemáticas: callAsFunction().

Ya os hablamos cuando se lanzó Swift 5 de algunas características que tenían que ver directamente con la implementación de Swift para Tensorflow.

¿Qué es esto? Es la implementación que está realizando Google sobre el lenguaje Swift para conseguir que este sea interoperable con Python, y a su vez, interoperable con cualquier API creada con la librería Tensorflow, de aprendizaje automático de Google.

¿Para qué querría hacer Google algo así? ¿No tienen ellos sus propios lenguajes? Sí, los tienen. Pero no son lenguajes que bajen a una capa de compilación binaria como serían C, C++, Swift… lo que se conoce como un lenguaje nativo binario 100%.

Lenguajes como Python o Dart (este último de la propia Google) no son lenguajes nativos. Python es un lenguaje de script, lo que supone que debe ser ejecutado en tiempo real (como Javascript) y los errores aparecen en el código al ser ejecutado. Al no compilarse, es menos eficiente pues tiene demasiadas capas hasta llegar a ejecutarse contra el código binario de la máquina.

En Dart, tenemos un caso similar a Java, ya que su funcionamiento en múltiples sistemas depende de una máquina virtual. Un software que se ejecuta contra la capa binaria de cada sistema y que traduce en tiempo real desde un código intermedio.

Por lo tanto, Google no tiene una opción propia de lenguaje 100% binario que usar para llevar un paso más allá a Tensorflow y conseguir que cualquier implementación de machine learning sea compilada. Si conseguimos compilar Python y traducirlo a binario, no solo estaremos detectando errores en el código. Estamos traduciendo este a un nivel de ejecución mucho más óptimo, que dará un salto cualitativo en velocidad y eficiencia a cualquier desarrollo de aprendizaje automático.

Swift, adaptándose a Python

Para que esta transformación suceda, Swift debe adaptarse a la gramática y usos de Python y de una forma más matemática (y dinámica) de ser invocado. No solo el lenguaje en sí, también a cómo funciona Tensorflow y a la estructura que le permite ser más matemático.

Por este motivo, Apple (que aceptó el proyecto de interoperabilidad de Python y Tensorflow de Google y lo incluyó en el proyecto de código abierto de Swift) adapta en ocasiones la forma en que se construyen las estructuras en Swift para que correspondan a las formas más habituales de trabajar con implementaciones matemáticas de aprendizaje automático y al lenguaje dinámico de funciones matemáticas.

Lo hicieron en su día con DynamicMemberLookup, una interesante forma de invocar propiedades que os contamos en un vídeo en el canal de Apple Coding que tenéis bajo estas líneas.

Explicamos la función DynamicMemberLookup en Swift 4.2

Esta función se introdujo en Swift 4.2, como podemos ver, siendo ampliada con las llamadas dinámicas o DynamicallyCall en Swift 5. También os hablamos de ello en Youtube y os dejamos el tutorial bajo estas líneas.

DynamicallyCall en Swift 5

Las llamadas dinámicas de función serían un siguiente paso hacia la programación dinámica y a gramáticas específicas que funcionan en la programación de modelos de aprendizaje automático.

Llamada como función o callAsFunction()

Esta función viene de la propuesta del lenguaje SE-0253 llamada Callable. Ha sido creado por Richard WeiDan Zheng y supervisada por el propio creador del lenguaje Chris Lattner.

Está promovida por la necesidad de incorporar más «sintáctica llamable» y su objetivo es cubrir la necesidad de representar valores como funciones dentro del lenguaje. Tanto con funciones matemáticas como funciones como expresiones simples. A su vez, este tipo de llamadas dinámicas permiten una interfaz más simple para llamadas más complejas en capas de redes neuronales, parseadores de datos y otras implementaciones complejas.

Vamos a ver un ejemplo simple para entenderlo. Imaginemos que tenemos una clase que nos permite cargar una imagen de forma remota.

final class ImageLoader {
    var url:URL
    var subscriber:AnyCancellable?
    var image:UIImage?
    
    init(url:URL) {
        self.url = url
    }
    
    func loadImage() {
        subscriber = URLSession.shared
            .dataTaskPublisher(for: url)
            .map {
                $0.data
            }
            .compactMap {
                UIImage(data: $0)
            }
            .sink(receiveCompletion: {
                if case .failure(let error) = $0 {
                    print("Error en la carga \(error)")
                }
            }, receiveValue: {
                self.image = $0
            })
        }
}

Esta clase simplemente carga una imagen de forma remota a través del uso de la librería Combine. Si no lo entendéis tenéis un tutorial que ya hicimos pulsando aquí. Es un tipo de implementación muy típica, ya que tenemos un constructor que da valor a los parámetros necesarios y luego hay una función secundaria diferente a la inicialización que es la que ejecuta la función principal.

Para invocar esta función y ver cómo funciona, solo tenemos que crear la instancia, pasarle el parámetro URL y luego invocar loadImage().

let load = ImageLoader(url: URL(string: "https://i.blogs.es/7ea98e/a-smartphone-in-close-up-view-3623360/840_560.jpg")!)
load.loadImage()
sleep(2)
load.image

Echamos a dormir 2 segundos el hilo principal con sleep(2) para que así le de tiempo al proceso en segundo plano a cargar la imagen de internet y que podamos verla en nuestro playground.

Captura del playground donde vemos cómo se ha cargado la imagen.
Captura del playground donde vemos cómo se ha cargado la imagen.

No tiene mayor complejidad y es un tipo de implementación muy común que usamos en nuestro día a día. Pero, ¿y si pudiéramos hacerla más intuitiva?

Vamos a cambiar el nombre de la función loadImage() por callAsFunction(). Nada más. Con ese simple gesto, ahora podemos llamar a nuestra carga de imagen así.

let loadDynamic = ImageLoaderDynamic(url: URL(string: "https://i.blogs.es/7ea98e/a-smartphone-in-close-up-view-3623360/840_560.jpg")!)
loadDynamic()
sleep(2)
load.image

Como veis, la llamada a la función ahora es simplemente llamar a la propia instancia loadDynamic como si fuera una función sin parámetros, no la instancia de una clase: usando loadDynamic(). Nada más.

Y obviamente, podemos hacer que esta función devuelva valores o que los reciba, como si fuera una función normal. Básicamente, estamos llamando a la función callAsFunction() de una manera implícita con solo llamar al nombre de la instancia donde está incluida.

Por ejemplo, podemos hacer un dado en un struct de la siguiente forma:

struct Dado {
    var caras:Int
    
    func callAsFunction() -> Int {
        Int.random(in: 1...caras)
    }
}


let dado1 = Dado(caras: 8)
let resultado = dado1()
resultado

Nada más simple y sencillo. Y sin duda, en mi opinión, bastante práctico. Es cierto que puede ser confuso para quien no conozca esta funcionalidad, pero a mi me parece una forma interesante de ahorrarse escribir código y usar las clases o estructuras más como una función matemática.

Una nueva e interesante aproximación de Swift a la gramática de lenguajes más matemáticos, al aprendizaje automático y la programación funcional dinámica.

Como siempre, podéis consultar el código de forma libre en nuestro GitHub, en la sección de Snippets. Espero que os suscribáis y le deis una estrella. Mientras, recordad que esto es Swift 5.2 y solo funciona en Xcode 11.4 Beta 1 o si bajáis el snapshot de Swift 5.2 de la página oficial y lo usáis cambiando el Toolchain en vuestro Xcode anterior.

Espero que os haya resultado interesante, un saludo y Good Apple Coding.

Etiquetas

Julio César Fernández

Analista, consultor y periodista tecnológico, desarrollador, empresario, productor audiovisual, actor de doblaje e ingeniero de vídeo y audio.

Artículos relacionados

Botón volver arriba
Cerrar
Cerrar