Francesco Perrotti-Garcia (@fpg1503) é desenvolvedor iOS. Atualmente trabalha na Moobie, já trabalhou em aplicativos como PlayKids, iFood e SpoonRocket. Programa desde os 12 anos e nos últimos 6 está cada vez mais próximo do desenvolvimento iOS. Swift mudou sua maneira de ver o mundo e até de como programar em Objective-C. Adora gatos e nas horas vagas gosta de viajar, cozinhar e tirar fotos. 🐥
O Que é?
Metaprogramação é uma palvara difícil simplesmente para dizer programas que manipulam programas. Especificamente neste artigo iremos falar sobre programas que escrevem programas (geração de código).
Por que fazer?
Você já se deparou copiando e colando código ou então fazendo um código extremamente verboso, com pouco signficado e propenso a erros? Já copiou e colou a chave errada ao serializar um JSON? Já consertou um bug em um lugar do código e depois descobriu que aquela trecho estava replicado por vários lugares e teve que sair caçando eles por aí?
Para mim programar é criar abstrações de problemas do mundo real e metaprogramação é a arte de abstrair uma abstração. Ao usar metaprogramação você escreve menos código repetitivo e, com isso, menos bugs.
Sourcery
Sourcery é uma ferramenta open source mantida pelo Krzysztof Zabłocki que alavanca metaprogramação em Swift usando templates. Não se preocupe, vou explicar o que isso significa nos próximos parágrafos.
O que é um template?
Template é uma palavra em inglês que pode ser traduzida como modelo ou gabarito. Imagine um template como um cortador de biscoitos, algo que dá forma ao que é colocado dentro, você pode usar um cortador de biscoitos em massinha de crianças mas isso não vai transformá-la em biscoito.
Por que templates?
A magia dos templates é desacoplar o formato da implementação de fato. Quando tempos um formato que diz como algo vai ser implementado basta mudar um lugar (o template) e a mudança é propagada. Além disso podemos fazer templates para diferentes versões da linguagem ou até mesmo para diferentes linguagens!
Stencil
Stencil é uma linguagem de templates para Swift criada e mantida pelo Kyle Fuller, a ideia é criar uma maneira de expressar a apresentação de algo. Farei uma introdução rápida ao Stencil porém encorajo você a dar uma lida na documentação oficial!
{{ ... }}
: imprime variáveis{% ... %}
: funciona para tags (mais sobre elas abaixo)
Tags
As duas tags mais importantes são for
e if
:
for
Suponhamos que há uma lista de usuários (chamada users
) e queremos listar todos eles um embaixo do outro:
Além disso podemos usar a tag empty
para lidar com listas vazias:
E por final temos à nossa disposição o contexto forloop
que possui três variáveis:
first
: boleano que indica se é a primeira iteração do looplast
: boleano que indica se é a última iteração do loopcounter
: iteração atual do loop
if
O if
avalia uma variável para verdadeira se um dos abaixo for válido:
- presente no contexto
- coleções: não vazias
- boleano: verdadeiro
- número: maior que zero
- string: não vazia
Filtros
Além disso Stencil possui alguns filtros (e Sourcery adiciona outros bem interessantes):
capitalize
: deixa a primeira letra da string em caixa alta e as demais em caixa baixa (Taylor Swift -> Tayor swift
)uppercase
: deixa todas as letras da string em caixa alta (Taylor Swift -> TAYLOR SWIFT
)lowercase
: deixa todas as letras da string em caixa baixa (Taylor Swift -> taylor swift
)
Filtros são aplicados a uma variável usando o pipe (|
), por exemplo: Taylor Swift
Há filtros que possuem parâmetros, esses parâmetros devem ser incluídos na forma variável|filtro:parâmetro
, um exemplo é o filtro join
em listas:
Além disso podemos usar as e filtros adicionadas pelo Sourcery e nas últimas versões você também pode usar os exportados pelo StencilSwfitKit.
Anotações
Infelizmente Swift não possui suporte a anotações de código, no entanto Sourcery traz uma alternativa para isso: comentários com /// sourcery
, veja o exemplo abaixo onde o struct User
é anotado AutoEquatable
:
Falaremos mais de como aproveitar anotações mais abaixo porém por enquanto é interessante saber que podemos usar um filtro de Stencil (annotated
) para encontrar apenas coisas anotadas, ou seja, se quisermos escrever algum código apenas para todas as variáveis anotadas AutoInject
entro de um tipo type
faríamos assim:
A beleza das anotações serem inseridas em comentários é que códigos anotados ainda são códigos Swift válidos que compilam normalmente!
Instalação
Instalar o Sourcery é muito simples e para brincar com ele é interessante baixar o binário e usar. Porém, para usá-lo dentro de projetos e garantir que todos estão na mesma versão você pode usar CoocaPods ou Swift Package Manager:
CocoaPods
Simplesmente adicione pod 'Sourcery'
na sua Podfile e $PODS_ROOT/Sourcery/bin/sourcery {source} {templates} {output}
em uma Build Phase de Script.
Swift Package Manager
Adicione a dependência e rode .build/debug/sourcery {source} {templates} {output}.
Para os exemplos a baixo eu recomendo que você tenha um binário preparado (na última versão) e três arquivos:
- Input.swift
- Template.stencil
- Output.swift
Eu sempre faço assim e crio um Script em shell para facilitar minha vida, seu conteúdo é somente:
Note que a flag --watch
acompanha seus arquivos e automaticamente regera a saída baseado nas mudanças, é mágico! Note que para isso funcionar eu tenho o sourcery na minha PATH
, caso você não tenha será necessário fornecer o caminho para o binário.
Editores de texto
Para poder acompanhar as mudanças em tempo real aconselho que você divida sua tela em duas partes: código sendo editado (template/fonte) e saída gerada automaticamente. Usando a flag --watch
que comentei acima basta salvar o arquivo que as mudanças são refletidas automaticamente
Sublime Text
Infelizmente o Sublime Text não atualiza arquivos abertos automaticamente quando há mudanças no disco então não recomendo o uso dele. Você poderia instalar algum plugin para isso porém a falta dessa feature inviabiliza o uso dele junto para visualizar mudanças automaticamente.
Atom
O Atom funciona incrivelmente bem para isso, além de permitir que você divida sua tela em vários panes em sentidos diferentes simultâneamente!
VSCode
Gosto muito do Visual Studio Code porém ele possui a limitação de só permitir a divisão em panes em um sentido (só vertical ou só horizontal). Esse não é um grande limitador para mim então costuma usar o VSCode com metade mostrando o código/template (2 abas) e a outra metade mostrando o código gerado.
Casos de Uso
Para todos os exemplos abaixo farei implementações simples, elas não cobrem todos os casos porém estará explicito em quais casos elas funcionam e uma referência para uma implementação que lida com todos os edge cases (caso ela exista). Nem sempre é necessário fazer um código ultra-complexo que cobre todos os casos possíveis e imagináveis, com metaprogramação você pode começar com um template que cumpra suas necessiades e ir evoluindo-o com o passar do tempo.
Equatable
Uma maneira simples de pensar em igualdade de tipos concretos é: todas as suas propriedades não computadas devem ser iguais. Essa implementação não lida com: Optionals, Enums, Arrays, Herança. Exemplo de implementação mais completa: AutoEquatable.
Para o struct User
:
Foi gerado o código:
AutoInjectable
Imagine que você possui um struct
com diversas propriedades porém quer que algumas delas sejam injetadas automaticamente e não quer perder o construtor que você ganhou, podemos para isso inserir uma anotação AutoInjectable
, nosso struct
ficaria assim:
Podemos escrever um template assim:
Código gerado:
No exemplo acima somente criamos um construtor de conveniência que chama nossa função capaz de prover dependências e junta isso com os parâmetros não injetados numa chamada para o construtor designado.
Desserialização de JSONs
Para desserialização de JSONs usaremos o protocolo JsonCreatable
que consiste de coisas que podem ser criadas a partir de um dicionário:
Nesse caso assumiremos que todas as propriedades não Optional
são obrigatórias, que todas as variáveis sem um nome pré-definido tem seu próprio nome no JSON e não lidamos com tipos diferentes de Números, Strings e Booleanos.
Nosso struct
:
Template:
Código gerado:
A beleza de usarmos templates é que não precisamos nos limitar a Swfit! Imagine que seu colega Android vai ter que implementar tudo de novo então você poderia escrever um template para ele!
Esse template gera o código abaixo:
Lindo, não? O único problema é que o código acima não compila pois os tipos que usamos tem nomes diferentes em Java, poderíamos criar um filtro customizado do Stencil (provavelmente chamado javaTypeName
) que fizesse essa conversão. O ponto deste exemplo é mostrar o quão flexível templates nos permitem ser!
Cliente HTTP
Como exemplo final vamos fazer um pequeno cliente HTTP de uma API Rest.
Dado uma interface da API (expressa em um protocol) queremos produzir uma implementação concreta. Para fazer isso usaremos o fato de que Sourcery copia o que inserimos dentro das anotações para nosso código. Usaremos dicionários de Swift para simular anotações de parâmetros.
Usando o template:
O código gerado neste caso é:
Repare como utilizamos os dicionários para mapear String
s para valores específicos que serão passados para a função, por isso nosso dicionário tem o formato ["chave": valor]
, fazendo isso teremos uma garantia em tempo de compilação de que os valores utilizados existem.
Cabe ressaltar não é possível acessar dentro do template as anoteações dos campos indvidualmente porém isso não é um problema pois temos uma extensão que permite expandir String
s usando dicionários. Além disso o código gerado conta com outras abstrações como a função genérica request
, a closure genérica Completion
, o enum HTTPMethod
e o protocolo Cancelable
, o código dessas abstrações não será incluso para manter o artigo sucinto porém elas podem facilmente ser subistituídas por outras de sua preferência.
Mais sobre Sourcery
Além de Stencil o Sourcery também permite o uso de SwiftTemplates e templates em JavaScript, usando o EJS.
Para saber mais soubre Sourcery dê uma lida no README do repositório, que contém mais informações sobre o que pode ser extraído de cada tipo e detalhes sobre como especificar o local de armazenamento do código gerado.
Outras ferramentas
Para trabalhar com Strings localizadas, Cores, Imagens, Storyboards e Fontes use o SwiftGen, uma ferramenta para gerar código e te ajudar a garantir (em tempo de compilação) que os recursos sendo utilizados de fato existem. SwiftGen também utiliza templates Stencil.
Em suma
Metaprogramação é uma ferramenta muito poderosa pois permite que você escreva menos código, código mais expressivo. Sourcery é uma ferramenta que faz com que você alavanque o sistema de tipos junto com o compilador para economizar tempo e reduzir potenciais erros. Além disso você sempre pode usar mais metapgromação: Se perceber que há algo que se repete muito você pode fazer um programa que faz um programa que faz um programa.
E você? Qual código faz no dia-a-dia que é repetitivo? Como você resolveu isso?