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

Santa Cruz JS Meetup Sweet.js Talk

By disnet

Santa Cruz JS Meetup Sweet.js Talk

  • 484
Loading comments...

More from disnet