Macros no JavaScript

Macros no ES8 ? Você não vai mais precisar criar um compilador para só ter uma sintaxe, ou um comportamento diferente no JavaScript.

O que seria um macro ?

Macros são ferramentas bem antigas, muito mesmo, e usadas em diversas formas, tanto para criação de DSL’s ou para automatizar algumas tarefas. Antigamente ela era muito usada, mas não é mais tão comum hoje. Na maioria das vezes é melhor evitá-las, mas elas são úteis ocasionalmente.

Quantos tipos de macros existem ?

Existe as textuais e sintáticas.

Textuais: Elas são bem simples, te permitem substituir algum texto por outro.

Sintáticas: Também fazem substituição, mas elas funcionam em elementos sintaticamente válido na linguagem que estiver usando. Muitas linguagens não tem esse suporte, e a mais famosa por isso é LISP, daí conseguimos estender a linguagem ao máximo que quisermos e assim também criar uma DSL.

Como seria isso em uma linguagem de programação ?

Antes de irmos direto com o JavaScript, vou primeiro dar alguns exemplos em outras linguagens para entendermos bem a ideia, e depois veremos como fazer com JavaScript.

Algumas funcionalidades de macros é: Stringficação e Concatenação.

Clojure por exemplo, é uma linguagem que não tem um for imperativo como C-style (i = 0; i < 3; i++).Mas podemos simular isso através de macros.

JavaScript não tem suporte?

Uma das recorrentes sugestões do que pode aparecer no ES8 são macros, não existe nenhuma certeza até o momento que eu saiba, mas o Brendan Eich já mencionou que é candidata, e acredito que com a chegada de macros no JavaScript, pode gerar um impacto bem grande na comunidade, pois macro é odiada e amada por muitos. Hoje pelo JavaScript ainda não ter suporte, podemos usar uma biblioteca da Mozilla chamada sweet.js, e com ela podemos simular macros ‘hygienic’ no JavaScript. A documentação dela é muito boa, vou mostrar um pouco da ideia de macros com ela, e na versão 0.7.8, a versão atual mudou bastante, é praticamente outra biblioteca, porém mais organizada e com integração com ES6 (mas ainda tá em fase de desenvolvimento e falta algumas coisas), fica a dica aí, mantenha os olhos nela, que tá saindo muita coisa interessante, e como a versão 0.7 já foi usada em diversos projetos pela comunidade e é o que você mais vai ver por aí, vou ensinar com ela. Como de regra, vamos com um olá mundo.

Hygienic Macro

Hygienic macros, são macros que te possibilita fazer a expansão garantida para não causar uma captura acidental de identificadores. Esse problema de captura acidental dos identificadoes é bem conhecido entre a comunidade LISP(quem mais sofreu com esse problema).

Mas o que seria exatamente isso? Se imaginarmos que macros é expandida para o local aonde ela está sendo usada, então também pode ocorrer que você já possa ter definido uma variável na sua macro e ter uma variável denifinida no local aonde for usar, e isso ocorre em um problema, pois ou ocorre erro, ou simplesmente substitui o valor da variável pelo novo.

Um sistema de macro em que isso não acontece, é chamado de hygienic!

Se você reparar ao compilar com sweet.js ele sempre renomeia as variaveis com $n, sendo que o n é um sufixo alternado, então, com isso você nunca vai ter problemas de uma variável ter sobrescrito a outra, por causa das declarações que tinha na macro.

O que faremos ?

Vamos extender a linguagem e criar uma sintaxe própria ou parecida com outras linguagens. E fica a dica para você que quer fazer um compilador javascript para resolver algum problema, ou até mesmo para brincar, pode ser que com macros, o seu problema possa ser resolvido!

Repare que esse código não é válido para a v8, mas vamos criar macros com que compilando, vire código válido (Imagine como se fosse um babel, CoffeeScript, ClojureScript, etc).

Criando macros

macro <name> {
rule { <pattern> } => { <template> }
}

Neste exemplo estou criando uma macro role infix, ou seja, você pode pegar dados anterior, usando o | para separar o lado esquerdo e direito para pegar cada expressão.

rule infix { $outer:expr | $inner:expr } => { ... }

Dei o nome de $outer, mas isso fica a sua escolha, apenas dei um nome que fizesse sentido, ou seja, a função da esquerda é a que fica fora e recebe outra função que dei o nome de $inner. Por exemplo: $outer($inner). Apoś o => {…} vou ter acesso e realizar o que desejo:

function() { return $outer($inner.apply(this, arguments)); }

E então posso fazer (double compose add)(2,3), que fará a soma e duplicar o resultado.

Criando novos operadores

O nosso código que precisamos compilar, usa o operador |>, equivalente ao pipe do Elixir, a qual você pega o que tá ao lado esquerdo e passa por parâmetro da direita:

// binary operators
operator <name> <precedence> <associativity>
{ <left operand>, <right operand> } => #{ <template> }

// unary operators
operator <name> <precedence>
{ <operand> } => #{ <template> }

O legal de criar operadores, é você deixar o código mais legível e ganhar produtividade, criei uma lib chamada Placeload.js aonde eu precisava ficar criando elementos e fazer algumas atribuições:

Isso eu consegui pois cada função dessa na verdade espera como argumento a sua propriedade mas retorna uma função que espera o elemento a ser passado e retorna o elemento com as propriedades que tinha definido, meio confuso ? Vamos ver:

var addClass = function (classname) {
return function (el) {
el.className += ' ' + classname;
return el;
}.bind(this);
};

Criando padrões alternativos

E se eu quiser que possa existir diversas formas de usar essa macro e ainda dizer o que fazer para cada caso?

Antes de continuarmos, quero chamar a atenção de você nessa macro. Perceba que estou querendo criar um comportamento para a palavra function, que é uma palavra reservada do javascript, e criei cases para todas as suas formas de declarações, e se eu não quero que fique recursiva, ainda mais sem fim, usei a palavra let.

// recursive form
macro foo { /* ... */ }
// non-recursive form
let foo = macro { /* ... */ }

Obtendo expressões

Os tokens que podem ser repetidos, você pode colocar (…) em uma expressão como 100 == foo, são 3 tokens, ou seja, você tem acesso a cada um, ou com (…) vc pega todos.

E se você quiser separar por uma virgula, para ter uma sintaxe semelhante a gettingXYZ(20, 2, 4):

macro gettingXYZ{
rule { ($x (,) ...) } => {
[$x (,) ...]
}
}
gettingXYZ (20, 2, 4)

Você pode tá achando que isso é inútil, visto que é só criar uma função, mas muitos desses padrões são poderosos quando eu misturo eles. Mais a frente vai ter exemplos um pouco mais prático da coisa.

Custom pattern classes

Imagine que você possa guardar seus padrões de forma que consiga modularizar suas macro, reutilizar e deixar mais legível?

Note que o if($first$check) o $check é o nome que foi dado para a expressão da esquerda e fica fácil fazer a manipulação pois fazemos a condição e como temos acesso ao $check, também temos acesso a $body, e inserimos dentro do if, esse processo pode se expandir a quanto o usuário quiser, e como vimos no exemplo anterior, como dizemos que uma expressão pode ser repetida inúmeras vezes? Usando o (…), por isso no final possui eles.

Exemplo prático com Tail Recursion

Recursão é uma maneira de resolver classes de problemas de forma elegante sem muitos esforços, mas um de seus maiores problemas é em sí o consumo de espaço na pilha, já que cada chamada é preciso criar um novo frame, e com isso funções profundamentes recursivas podem esgotar mais rápido a memória. Mas isso pode ser resolvido através de uma otimização chamada tail call otimization, a qual é bem comum em linguagens funcionais, aonde é comum o uso de recursão. E você pode simular isso com JavaScript, já que ao invés de precisar esperar o resultado da chamada posterior, eu possa ir acumulando o resultado. Nesse exemplo vou usar algumas técnicas que foram utilizada nos exemplos anteriores, e vamos criar de uma forma que eu possa simplesmente avisar a função recursiva que vou precisar. Exemplos com várias maneiras de declaração:

Usando módulos

Você pode criar suas macros e organizar em pastas ou arquivos e exportar, e na hora que for compilar com sweet.js o seu fonte, apenas explicite o módulo que foi usado:

// macros.js

macro p { /* ... */ }
export p;

e o seu fonte:

olamundo.js
p "Ola mundo"

E para compilar é assim:

$ sjs --module ./macros.js  olamundo.js

Com isso, abre a possibilidade de você criar macros e compartilhar com a comunidade, como por exemplo a lambda-chop onde tem um conjunto de macros para lambdas com currying, bound functions, e placeholders. Como viu em alguns exemplos utilizando λ. Clique aqui para mais exemplos com o lambda-chop.

var curry3 = λ fn a b c -> fn(a, b, c);
out:
var curry3 = function ( fn ) {
return function ( a ) {
return function ( b ) {
return function ( c ) {
return ( fn ( a , b , c ) )
}
}
}
}

E não acaba por aí, existe diversos e diversos módulos, outro por exemplo é es6-macros, onde você tem algumas macros com suporte em algumas features do es6 compilando para es5.

DSL

Deu pra perceber o quanto é flexível macros? Você simplesmente pode criar padrões e padrões, até chegar no que você deseja, até então criar uma DSL por exemplo, ou resolver um exato problema. Estou em uma equipe a qual tivemos um problema em que o sweet.js foi uma possibilidade da resolução, mas por alguns motivos , foi decidido o uso do recast que possui uma interface de customização para ESTree por meio de um middleware. Agora vamos imaginar um problema, você está em um projeto(de cálculo) aonde você quer que o usuário informe a equação e você disponibiliza alguns operadores, como ^, λ, fn, etc, ou até mesmo que possibilite ele fazer a sua sintaxe:

square 2 ^4 |> λ x return remainder x <|> 10

Existe diversas possibilidades de resover esse problema, e macros é uma delas, criando macros para as expressões e operadores da sintaxe que você está disponibilizando para o projeto, ou seja, pegue o que ele digitou no arquivo e compile, assim você pode trazer uma experiência melhor para ele e fazer parecer um processo bem simples, e com isso você acaba criando uma DSL.

Conclusão

Espero que esses exemplos tenham servido para mostrar um pouco do poder que macros tem, e é até importante saber de sua existência, pois você vai saber quando o problema pode ser resolvido apenas com macros, e não criando um compilador(que traz mais custos).