Swift GYB
El término boilerplate se remonta a los inicios de la prensa escrita. Los pequeños periódicos regionales tenían espacios de columnas que rellenar, pero carecían normalmente del personal necesario para hacerlo, por lo que muchos de ellos recurrieron a grandes sindicatos de impresión para proveerse de contenido que pudiera añadirse a las últimas páginas de los diarios. A menudo, estas historias se proporcionaban en forma de placas metálicas preestablecidas, que se parecían a las láminas de acero utilizadas para hacer calderos o boilers (en inglés). De ahí el nombre.
A través de un proceso de metonimia, el contenido en sí pasó a conocerse como boilerplate, y el concepto fue utilizado para referirse a los textos formulados y estandarizados de los contratos, plantillas y, relevante al artículo de esta semana en NSHipster, código.
No todo el código puede ser glamuroso. De hecho, mucha de la infraestructura de bajo nivel que hace que todo funcione es, a veces, un amasijo repetitivo.
Esto se cumple en la librería estándar de Swift, la cual incluye familias de tipos como los enteros con signo (Int8
, Int16
, Int32
, Int64
), cuya implementación varía únicamente en el tamaño del tipo respectivo.
Copiar y pegar código puede funcionar como una solución puntual (asumiendo que logras hacerlo bien la primera vez), pero no es sostenible. Cada vez que quieres hacer un cambio a una de estas implementaciones similares te arriesgas a introducir ligeras inconsistencias que harán que las implementaciones diverjan con el tiempo; no muy distintas a las mutaciones aleatorias responsables de la diversidad de la vida en la Tierra.
Los lenguajes de programación tienen diversas técnicas para lidiar con esto, desde los templates de C++ y las macros de Lisp a eval
y las sentencias del preprocesador de C.
Swift no tiene un sistema de macros, y como la librería estándar está escrita en este lenguaje, no puede aprovechar las capacidades de metaprogramación de C++. En su lugar, los que mantienen Swift usan un script en Phyton llamado gyb.py para generar código fuente usando un pequeño conjunto de etiquetas.
GYB es un acrónimo de “Generate Your Boilerplate”, y hace referencia a otra herramienta de Phyton llamada GYP (“Generate Your Projects”).
Cómo funciona GYB
GYB is un sistema de plantillas que te permite usar código Phyton para sustituir variables y controlar el flujo:
- La secuencia
%{ code }
evalúa un bloque de código Phyton. - La secuencia
% code: ... % end
gestiona control de flujo. - La secuencia
${ code }
sustituye el resultado de una expresión.
El resto del texto se ignora.
Un buen ejemplo de GYB puede encontrarse en Codable.swift.gyb. Al principio del fichero, los tipos básicos de Codable
se asignan a una variable de instancia:
%{
codable_types = ['Bool', 'String', 'Double', 'Float',
'Int', 'Int8', 'Int16', 'Int32', 'Int64',
'UInt', 'UInt8', 'UInt16', 'UInt32', 'UInt64']
}%
Después, en la implementación de Single
, estos tipos se iteran para generar las declaraciones de los métodos requeridas por el protocolo:
% for type in codable_types:
mutating func encode(_ value: ${type}) throws
% end
Evaluar la plantilla GYB resulta en:
mutating func encode(_ value: Bool) throws
mutating func encode(_ value: String) throws
mutating func encode(_ value: Double) throws
mutating func encode(_ value: Float) throws
mutating func encode(_ value: Int) throws
mutating func encode(_ value: Int8) throws
mutating func encode(_ value: Int16) throws
mutating func encode(_ value: Int32) throws
mutating func encode(_ value: Int64) throws
mutating func encode(_ value: UInt) throws
mutating func encode(_ value: UInt8) throws
mutating func encode(_ value: UInt16) throws
mutating func encode(_ value: UInt32) throws
mutating func encode(_ value: UInt64) throws
Este patrón se usa a lo largo del fichero para generar declaraciones similares para métodos como encode(_:for
, decode(_:for
y decode
. En total, GYB reduce la cantidad de código repetitivo en unas miles de líneas de código:
$ wc -l Codable.swift.gyb
2183 Codable.swift.gyb
$ wc -l Codable.swift
5790 Codable.swift
Importante: Una plantilla GYB válida podría llegar a generar código Swift inválido. Si un error de compilación ocurre en un fichero, puede ser complicado determinar la causa subyacente.
Usando GYB en Xcode
GYB no es parte de la toolchain estándar de Xcode, así que no lo encontrarás en xcrun
. Sin embargo, puedes descargar el código fuente y usar el comando chmod
para hacer gyb
ejecutable (la instalación de Phyton por defecto en macOS debería poder ejecutar gyb
):
$ wget https://github.com/apple/swift/raw/master/utils/gyb
$ wget https://github.com/apple/swift/raw/master/utils/gyb.py
$ chmod +x gyb
Mueve estos ficheros a algun lugar accesible de tu proyecto Xcode, pero mantenlos separados de tus ficheros fuente. Por ejemplo, en un directorio Vendor
en la raíz del proyecto.
En Xcode, haz click en el archivo azul del proyecto en el navegador, selecciona el target activo del proyecto y navega al panel de “Build Phases”. Arriba del todo verás un símbolo +
en el que puedes hacer click para añadir una nueva. Selecciona “Add New Run Script Phase” e introduce lo siguiente en el editor:
find . -name '*.gyb' | \
while read file; do \
./path/to/gyb --line-directive '' -o "${file%.gyb}" "$file"; \
done
Asegúrate de que la build phase GYB esté antes de “Compile Sources”.
Ahora cuando construyas el proyecto, cualquier fichero con la extensión .swift.gyb
será evaluado por GYB, que generará un fichero .swift
que se compilará junto con el resto del código del proyecto.
Cuándo usar GYB
Como con cualquier herramienta, saber cuándo usarla es tan importante como saber usarla. Aquí tienes algunos ejemplos de cuándo podrías abrir tu caja de herramientas y usar GYB.
Generando código
¿Estás constantemente copiando y pegando el mismo código para los elementos de un set o los ítems de una secuencia? Un bucle for-in con sustitución de variables podría ser la solución.
Como vimos en el ejemplo de Codable
anterior, puedes declarar una colección al principio de tu plantilla GYB e iterar sobre la colección por tipos, propiedades or declaraciones de métodos:
%{ abilities = ['strength', 'dexterity', 'constitution',
'intelligence', 'wisdom', 'charisma']
}
class Character {
var name: String
% for ability in abilities:
var ${type}: Int
% end
}
Ten presente que mucha repetición es un claro síntoma de que algo se puede mejorar. Algunas características del lenguaje como las extensiones de protocolo y los genéricos pueden eliminar mucha duplicación de código, y son mucho mejores que aplicar una solución de fuerza bruta como GYB.
Generando código a partir de datos
¿Estás escribiendo código a partir de una fuente de datos? </br> ¡Intenta aplicar GYB!
Los ficheros GYB pueden importar paquetes de Phyton como json
, xml
y csv
, por lo que puedes analizar casi cualquier tipo de fichero con el que te puedas topar:
%{ import csv }
% with open('path/to/file.csv') as file:
% for row in csv.Dict Reader(file):
Si quieres ver esto en acción, echa un vistazo a Currencies.swift.gyb, el cual genera enums para cada moneda definida en la especificación ISO 4217.
Mantén la compilación rápida y determinista cargando datos en ficheros que puedan integrarse en el control de versiones en vez de estar haciendo peticiones HTTP o consultas a la base de datos desde ficheros GYB.
La generación de código simplifica el tener el código en sintonía con los últimos requisitos. Simplemente actualiza el fichero de datos y vuelve a ejecutar GYB.
Recientemente, Swift ha hecho mucho por minimizar el código repetitivo gracias a la sintetización que hace el compilador de Encodable
y Decodable
en 4.0,
Equatable
y Hashable
en 4.1, y
Case
en 4.2.
Esperemos que este impulso se mantenga en futuras versiones del lenguaje.
Mientras tanto, GYB es una herramienta muy útil para la generación de código.
Otra herramienta genial de la comunidad es Sourcery, que permite escribir plantillas en Swift (via Stencil) en lugar de Python.
No repetirte será una virtud en programación, pero a veces hay que decir las cosas unas cuantas veces para que funcionen. Y cuando lo hagas, agradecerás que una herramienta como GYB las diga por ti.