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");
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
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/cli
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.expand('expr').value;
return #`
(function (${ident}) {
${ctx}
}(${init}))
`
}
let bb8 = new Droid('BB-8', 'orange');
console.log(bb8.beep());
class macro
import { unwrap, isIdentifier }
from '@sweet-js/helpers' for syntax;
syntax class = function (ctx) {
let name = ctx.next().value;
let bodyCtx = ctx.contextify(ctx.next().value);
// default constructor if none specified
let construct = #`function ${name} () {}`;
let result = #``;
for (let item of bodyCtx) {
if (isIdentifier(item) && unwrap(item).value === 'constructor') {
construct = #`
function ${name} ${bodyCtx.next().value}
${bodyCtx.next().value}
`;
} else {
result = result.concat(#`
${name}.prototype.${item} = function
${bodyCtx.next().value}
${bodyCtx.next().value};
`);
}
}
return construct.concat(result);
};
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
}
import { unwrap, isKeyword }
from '@sweet-js/helpers' for syntax;
syntax cond = function (ctx) {
let bodyCtx = ctx.contextify(ctx.next().value);
let result = #``;
for (let stx of bodyCtx) {
if (isKeyword(stx) &&
unwrap(stx).value === 'case') {
let test = bodyCtx.expand('expr').value;
// eat `:`
bodyCtx.next();
let r = bodyCtx.expand('expr').value;
result = result.concat(#`${test} ? ${r} :`);
} else if (isKeyword(stx) &&
unwrap(stx).value === 'default') {
// eat `:`
bodyCtx.next();
let r = bodyCtx.expand('expr').value;
result = result.concat(#`${r}`);
} else {
throw new Error('unknown syntax: ' + stx);
}
}
return result;
};
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;
log 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!');
}
Custom operators
operator >>= left 1 = (left, right) => {
return #`${left}.then(${right})`;
};
fetch('/foo.json') >>= resp => { return resp.json() }
>>= json => { return processJson(json) }
fetch("/foo.json").then(resp => {
return resp.json();
}).then(json => {
return processJson(json);
});
Parser combinators
import * as C from '@sweet-js/helpers/combinators' for syntax;
import { Either } from '@sweet-js/helpers/either' for syntax;
syntax class = ctx => {
let comma = C.punctuatorWith(v => v === ',');
let method = C.sequence(
C.identifier,
C.parensInner(C.sepBy(C.identifier, comma)),
C.braces
);
let result = C.sequence(
C.identifier,
C.bracesInner(C.many(method))
).run(ctx);
if (Either.isRight(result)) {
return #`'Parse success!'`;
}
throw new Error('parsing failed!!');
}
macros enable you
to build the future
help us build the future
- npm install -g @sweet-js/cli
- https://sweetjs.org
- https://github.com/sweet-js/sweet-core
SJSU Sweet.js Talk
By disnet
SJSU Sweet.js Talk
- 1,044