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

SJSU Sweet.js Talk

By disnet

SJSU Sweet.js Talk

  • 1,044