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>
#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>
#define show_str(x) printf(#x " é uma string ! \n");
#define conc(x,y) x ## y
int main()
{
show_str(Hello World )
int conc(lol,me) = 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#))))
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}
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!
var appendIn = λ el -> λ x -> el.appendChild(x);
var elbody = document.querySelector('body');
var container = document.createElement('div') |> appendIn(elbody);
var sum = λ n1 -> λ n2 -> n1 + n2;
var sum10 = sum(10);
console.log('Sum 10 + 90 ', sum10(90));
var a = 7 in [1,2,3,4];
var isEvenNaiveS2 = tailFunction (num) {
if (num === 0) {
return true;
}
if (num === 1) {
return false;
}
return isEvenNaiveS2(Math.abs(num) - 2);
}
console.log(isEvenNaiveS2(99999));
var add = λ(x,y) -> x + y;
var double = λ x -> x * 2;
console.log((double compose add)(2,3));
var y = 12;
assert(99 == y);
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))
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:
operator <name> <precedence> <associativity>
{ <left operand>, <right operand> } => #{ <template> }
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 ))
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});
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 = λ 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.
macro foo { }
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);
assert(99 == foo);
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?
macroclass cond_clause {
pattern {
rule { $check:expr => $body:expr }
}
}
macro cond {
rule { $first:cond_clause $rest:cond_clause ... } => {
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")
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.
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:
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 {
case infix {$name :|tailFunction ($args (,) ...) { $body ... }} => {
return #{$name: tco(function ($args (,) ...){
$body ...
})}
}
case {_ ($args (,) ...) { $body ... }} => {
return #{tco(function ($args (,) ...){
$body ...
})}
}
case {_ $name ($args (,) ...) { $body ... }} => {
return #{function $name(){
if(! $name.__tco) {
$name.__tco = tco(function ($args (,) ...) {
$body...
});
}
return $name.__tco.apply($name.__tco, arguments);
}}
}
}
tailFunction isEvenNaiveS1 (num) {
if (num === 0) {
return true;
}
if (num === 1) {
return false;
}
return isEvenNaiveS1(Math.abs(num) - 2);
}
console.log(isEvenNaiveS1(99999));
var isEvenNaiveS2 = tailFunction (num) {
if (num === 0) {
return true;
}
if (num === 1) {
return false;
}
return isEvenNaiveS2(Math.abs(num) - 2);
}
console.log(isEvenNaiveS2(99999));
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));
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:
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);
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