Macros 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.

#include<stdio.h>
//algumas macros definidas
#define print printf(
#define end );

int main()
{
print "Hello World !\n" end
print "This is a Macro .\n" end
return 0;
}

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

#include<stdio.h>
//Stringificação
#define show_str(x) printf(#x " é uma string ! \n");
//Concatenaçãoo
#define conc(x,y) x ## y

int main()
{
show_str(Hello World )
int conc(lol,me) = 4;
//Nesse passo a linha acima será substituida por:
//int lolme = 4;
printf("lolme = %d\n", conc(lol,me));

return 0;
}
view raw

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.

(defmacro forL [[sym init check change :as params] & steps]
(cond
(not (vector? params))
(throw (Error. "A forma de dec deve ser um vetor"))
(not= 4 (count params))
(throw (Error. "Ele precisa ter 4 argumentos"))
:default
`(loop [~sym ~init value# nil]
(if ~check
(let [new-value# (do ~@steps)]
(recur ~change new-value#))
value#))))
;; Como usar ?
;; (forL [i 0, (< i 10), (inc i)]
;; (println i))

JavaScript não tem suporte? #

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.

macro p {
case{ _ $x } => {
return #{ console.log($x); }
}
}

p "Hello world"

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 identificadores é 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 definida 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!

var tmp = 10;
var b = 20;

swap {tmp <=> b}

/* --> hygienic expansion
var tmp$1 = 10;
var b$2 = 20;
var tmp$3 = tmp$1;
tmp$3 = b$2;
b$2 = tmp$1;
*/

Se você reparar ao compilar com sweet.js ele sempre renomeia as variáveis 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!

//dom
var appendIn = λ el -> λ x -> el.appendChild(x);
var elbody = document.querySelector('body');
var container = document.createElement('div') |> appendIn(elbody);

//math
var sum = λ n1 -> λ n2 -> n1 + n2;
var sum10 = sum(10);
console.log('Sum 10 + 90 ', sum10(90));

//collections
var a = 7 in [1,2,3,4]; // 7 contain in array ? true or false

//tail otimization
var isEvenNaiveS2 = tailFunction (num) {
if (num === 0) {
return true;
}

if (num === 1) {
return false;
}

return isEvenNaiveS2(Math.abs(num) - 2);
}

console.log(isEvenNaiveS2(99999));


//function compose
var add = λ(x,y) -> x + y;
var double = λ x -> x * 2;
console.log((double compose add)(2,3));

var y = 12;
//assert
assert(99 == y); //fail!
/*
Error: AssertionError 99 == foo
*/

//conditions
var x = 20;
cond
x < 20 => console.log("less than 20")
x == 20 => console.log("20")
x > 20 => console.log("greater than 20")

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> }
}
macro compose {
rule infix {
$outer:expr | $inner:expr
} => {
function() {
return $outer($inner.apply(this, arguments));
}
}
}

function add(x,y) { return x + y; }
function double(x) { return x * 2; }
console.log((double compose add)(2,3))

/*
function add(x, y) {
return x + y;
}
function double2(x) {
return x * 2;
}
console.log(function () {
return double(add.apply(this, arguments));
}(2, 3));
> 10
*/

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> }
operator (|>) 1 left { $left, $right } => #{
$right($left)
}

function add(x,y) { return x + y; }
function double(x) { return x * 2; }

console.log((add(2,3) |> double ))
/*
function add(x$, y) {
return x + y;
}
function double(x) {
return x * 2;
}
console.log(double(add(2, 3)));
*/

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:

this.marginTopElement = document.createElement('div')
|> appendIn(animateContentEl)
|> addClass('placeload-masker')
|> size('100%', dataDefault.marginTop)
|> position({top: this.fullHeight, left: 0});

/*
this.marginTopElement = position({
top: this.fullHeight + 'px',
left: 0
})(size('100%', dataDefault.marginTop)
(addClass('placeload-masker')
(appendIn(animateContentEl)
(document.createElement('div')))));
*/

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:


/* fn anonima esperando o nome da classe, e retorna uma fn esperando o elemento ser passado,
e então, retorna o elemento com a classe adicionada. */

var addClass = λ classname -> λ el => {
el.className += ' ' + classname;
return el;
};
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?

let function = macro {
case {
$fun $name ( $args ...) { $body ... }
} => {
return #{
function $name ( $args ...) {
console.log($name.name)
$body ...
}
}
}

case {
$fun ($args ... ) { $body ...}
} => {
return #{
function( $args ...){
console.log("Anonymous function")
$body ...
}
}
}
}

function myfun () {
console.log('Foo')
}
var time = setTimeout(function () {
console.log('barr')
}, 10);

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.

macro assert {
rule {($test ...)} => {
if (!($test ...)) {
throw new Error('AssertionError: '.concat($test ...));
}
}
}

var foo = 100;

assert(100 == foo);
// passes

assert(99 == foo);
// fails

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?

// define the cond_clause pattern class
macroclass cond_clause {
pattern {
rule { $check:expr => $body:expr }
}
}

macro cond {
rule { $first:cond_clause $rest:cond_clause ... } => {
// sub-pattern variables in the custom class are
// referenced by concatenation
if ($first$check) {
$first$body
} $(else if ($rest$check) {
$rest$body
}) ...
}
}

cond
x < 3 => console.log("less than 3")
x == 3 => console.log("3")
x > 3 => console.log("greater than 3")
// expands to:
// if (x < 3) {
// console.log('less than 3');
// } else if (x == 3) {
// console.log('3');
// } else if (x > 3) {
// console.log('greater than 3');
// }

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:

/*
Sweet.js version: 0.7.0
node.js 0.10.30

more info about tco function at: https://gist.github.com/Gozala/1697037
*/

var tco = function (f) {
var value, active = false, accumulated = [];

return function accumulator() {
accumulated.push(arguments);

if (!active) {
active = true;
while (accumulated.length) {
value = f.apply(this, accumulated.shift());
}

active = false;
return value;
}

};
};


let tailFunction = macro {
/*
function inside object
var o = {
example: tailFunction () {...}
}
*/

case infix {$name :|tailFunction ($args (,) ...) { $body ... }} => {
return #{$name: tco(function ($args (,) ...){
$body ...
})}
}
/*
function expression
var example = tailFunction () {...}
*/

case {_ ($args (,) ...) { $body ... }} => {
return #{tco(function ($args (,) ...){
$body ...
})}
}
/*
function declaration and named function expression
tailFunction example () {...}
var example = tailFunction example () {...}
*/

case {_ $name ($args (,) ...) { $body ... }} => {
return #{function $name(){
if(! $name.__tco) {
$name.__tco = tco(function ($args (,) ...) {
$body...
});
}
return $name.__tco.apply($name.__tco, arguments);
}}
}
}

/*
Recognized Syntax
insipred from: http://us6.campaign-archive2.com/?u=2cc20705b76fa66ab84a6634f&id=9bd21a4dce
*/


// Function declaration
tailFunction isEvenNaiveS1 (num) {
if (num === 0) {
return true;
}

if (num === 1) {
return false;
}

return isEvenNaiveS1(Math.abs(num) - 2);
}

console.log(isEvenNaiveS1(99999));

// Function expression
var isEvenNaiveS2 = tailFunction (num) {
if (num === 0) {
return true;
}

if (num === 1) {
return false;
}

return isEvenNaiveS2(Math.abs(num) - 2);
}

console.log(isEvenNaiveS2(99999));

// Inside object
var o = {
isEvenNaiveS3: tailFunction (num) {
if (num === 0) {
return true;
}

if (num === 1) {
return false;
}

return o.isEvenNaiveS3(Math.abs(num) - 2);
}
};

console.log(o.isEvenNaiveS3(99999));

// Named function expression
var isEvenNaiveS4 = tailFunction isEvenNaiveS4(num) {
if (num === 0) {
return true;
}

if (num === 1) {
return false;
}

return isEvenNaiveS4(Math.abs(num) - 2);
}

console.log(isEvenNaiveS4(99999));

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 complexidade).

🙏🙏🙏

Já que você chegou até aqui, seria muito show compartilhar este artigo em sua rede social favorita 💖! Para feedback, comente ou interaja com emoji 👻

Published