Hygienic Macros for JavaScript
Tim Disney
@disnet

Javascript syntax is
sour
Making Droids

function Droid(name, color) {
this.name = name;
this.color = color;
}
Droid.prototype.rollWithIt = function(it) {
return this.name + " is rolling with " + it;
};
var bb8 = new Droid("BB-8", "orange");
bb8.rollWithIt("the force");
Making Droids
function Droid(name, color) {
this.name = name;
this.color = color;
}
Droid.prototype.rollWithIt = function(it) {
return this.name + " is rolling with " + it;
};
var bb8 = new Droid("BB-8", "orange");
bb8.rollWithIt("the force");
droid is a function?
proto-what?
why not just class?

ES
2015
making it classy
class Droid {
constructor(name, color) {
this.name = name;
this.color = color;
}
rollWithIt(it) {
return this.name + " is rolling with " + it;
}
}
var bb8 = new Droid("BB-8", "orange");
bb8.rollWithIt("the force");
Number of years I've
used this example: 3
Number of shipping desktop
browsers with class: 2 (ish)



macros
sweeten
syntax now
sweet.js
- A hygienic macro system for JavaScript
- A compiler (or transpiler if you like) for JavaScript+macros
- Gives "normal" programmers the power to create syntax abstractions
- Began life as a internship project at Mozilla in 2012
- (the version of sweet I'm showing today is a pre-release complete redesign)
Class Macro
import { class } from 'es2015-macros';
class Droid {
constructor(name, color) {
this.name = name;
this.color = color;
}
rollWithIt(it) {
return this.name +
" is rolling with " + it;
}
}
var bb8 = new Droid("BB-8", "orange");
bb8.rollWithIt("the force");
function Droid(name, color) {
this.name = name;
this.color = color;
}
Droid.prototype.rollWithIt = function(it) {
return this.name + " is rolling with " + it;
};
var bb8 = new Droid("BB-8", "orange");
bb8.rollWithIt("the force");
sjs bb8.js
npm install -g sweet.js@1.0.0-alpha.14
npm install es2015-macros
what about
- Some similar goals
- (sweet uses babel as a backend)
- babel can't support composable abstractions

conditional issues
import { class } from 'es2015-macros';
class Droid {
constructor(name, color) {
this.name = name;
this.color = color;
}
rollWithIt(it) {
return this.name + " is rolling with " + it;
}
greet(other) {
return other instanceof Droid ? '[beeps excitedly]' :
other instanceof Sith ? '[beeps fearfully]' :
'Beep-Boop'
}
}
}
conditional issues
import { class } from 'es2015-macros'
import { cond } from 'cond-macro'
class Droid {
constructor(name, color) {
this.name = name;
this.color = color;
}
rollWithIt(it) {
return this.name + " is rolling with " + it;
}
greet(other) {
return cond {
case other instanceof Droid: '[beeps excitedly]'
case other instanceof Sith: '[beeps fearfully]'
default: 'Beep-Boop'
}
}
}
macros
are
composable
Making macros
a new creation
new Droid('BB-8', 'orange');
Droid.create('BB-8', 'orange');
lex
parse
eval
new Droid('BB-8', 'orange')
new
Droid
(
'BB-8'
,
'orange'
)
Lexemes
AST
NewExpr
Callee
IdentExpr
Droid
Arguments
LitStrExpr
LitStrExpr
'BB-8'
'orange'
Traditional Compiler
read
parse
eval
new Droid('BB-8', 'orange')
new
Droid
(
'BB-8'
,
'orange'
)
Syntax
AST
NewExpr
Callee
IdentExpr
Droid
Arguments
LitStrExpr
LitStrExpr
'BB-8'
'orange'
delimiter
Sweet.js Compiler
a new creation
syntax new = function (ctx) {
}
new Droid('BB-8', 'orange');
Droid.create('BB-8', 'orange');
syntax <name> = <trans>
syntax new = function (ctx) {
let ident = ctx.next().value;
let params = ctx.next().value;
return #`${ident}.create ${params}`;
}
new Droid('BB-8', 'orange');
#`...` :: List(Syntax)
trans :: Iterator → List(Syntax)
exporting a new creation
export syntax new = function (ctx) {
let ident = ctx.next().value;
let params = ctx.next().value;
return #`${ident}.create ${params}`;
}
export syntax is just like export var/let/const and make macros available for import
letting a new creation
import { new } from 'new-macro';
syntax let = function (ctx) {
}
let bb8 = new Droid('BB-8', 'orange');
console.log(bb8.beep());
(function(bb8) {
console.log(bb8.beep());
})(Droid.create("BB-8", "orange"));
import { new } from 'new-macro';
syntax let = function (ctx) {
let ident = ctx.next().value;
// eat `=`
ctx.next();
let init = ctx.next('expr').value;
let rest = Array.from(ctx);
return #`
(function (${ident}) {
${rest}
}(${init}))
`
}
let bb8 = new Droid('BB-8', 'orange');
console.log(bb8.beep());
cond macro
let x = null;
let realTypeof = cond {
case x === null: 'null'
case Array.isArray(x): 'array'
case typeof x === 'object': 'object'
default: typeof x
}
syntax cond = function (ctx) {
let bodyCtx = ctx.of(ctx.next().value);
let result = [];
for (let stx of bodyCtx) {
if (stx.isKeyword() && stx.val() === 'case') {
let test = bodyCtx.next('expr').value;
// eat `:`
bodyCtx.next();
let r = bodyCtx.next('expr').value;
result.push(#`${test} ? ${r} :`);
} else if (stx.isKeyword() &&
stx.val() === 'default') {
// eat `:`
bodyCtx.next();
let r = bodyCtx.next('expr').value;
result.push(#`${r}`);
} else {
throw new Error('unknown syntax: ' + stx);
}
}
return result.reduce(function (acc, l) {
return acc.concat(l);
});
}
class macro
syntax class = function (ctx) {
let name = ctx.next().value;
let bodyCtx = ctx.of(ctx.next().value);
let construct;
let methods = [];
for (let item of bodyCtx) {
if (item.isIdentifier() &&
item.val() === 'constructor') {
construct = #`
function ${name} ${bodyCtx.next().value}
${bodyCtx.next().value}
`;
} else {
methods.push(#`
${name}.prototype.${item} = function
${bodyCtx.next().value}
${bodyCtx.next().value};
`);
}
}
return methods.reduce(function (acc, method) {
return acc.concat(method);
}, construct);
}
hygiene
(literally the most important thing)
hygiene means macros are
abstractions
swap macro
syntax swap = function (ctx) {
let a = ctx.next().value;
// eat <>
ctx.next();
ctx.next();
let b = ctx.next().value;
return #`
var tmp = ${a};
${a} = ${b};
${b} = tmp;
`;
}
var x = 10,
tmp = 20;
swap x <> tmp
var x = 10,
tmp = 20;
var tmp = x;
x = tmp;
tmp = tmp;
Unhygienic
hygienic
var x = 10,
tmp = 20;
var tmp2 = x;
x = tmp;
tmp = tmp2;
swap macro
syntax logger = function (ctx) {
let msg = ctx.next().value;
return #`
console.log ${msg};
`
}
function sympathy(console) {
logger('attempting to help out...');
console('you are amazing!');
}
function sympathy(console) {
console.log('attempting to help out...');
console('you are amazing!');
}
Unhygienic
hygienic
function sympathy(console2) {
console.log('attempting to help out...');
console2('you are amazing!');
}
coming soon
coming soon
- declarative pattern matching (macro-by-example)
syntax new = function (ctx) {
return match (ctx) {
case`new ${ident} ${params}` => #`
${ident}.create ${params}
`
}
}
syntax let = function (ctx) {
return match (ctx) {
case`let ${ident} = ${init}:expr
${rest} ...`
=> #`
(function (${ident}) {
${rest} ...
})(${init})
`
}
}
coming soon
- declarative pattern matching (macro-by-example)
- custom operators
operator ** 14 right = function (left, right) {
return #`Math.pow(${left}, ${right})`;
}
y + x ** 10 ** 100 - z
y + Math.pow(x, Math.pow(10, 100)) - z;
coming soon
- declarative pattern matching (macro-by-example)
- custom operators
- import for syntax
import * as _ from 'ramda' for syntax;
import Syntax from 'sweet.js' for syntax;
syntax range = function (ctx) {
let stop = ctx.next().value.val();
let toStx = _.map(Syntax.fromNumber);
let addComma = _.intersperce(
Syntax.fromPunctuator(',')
);
let wrapBrackets = stx => #`[${stx}]`;
return _.pipe(
toStx,
addComma,
wrapBrackets
)(_.range(0, stop));
}
range 10
[0, 1 , 2, 3, 4, 5, 6, 7, 8, 9]
who
decides
the future?
TC-39
macros
YOU!






macros enable you
to build the future
help us build the future
- npm install -g sweet.js@1.0.0-alpha.14
- https://github.com/mozilla/sweet.js
Santa Cruz JS Meetup Sweet.js Talk
By disnet
Santa Cruz JS Meetup Sweet.js Talk
- 1,393