OSCON - ES6 metaprogramming unleashed

55
ES6 metaprogramming unleashed

Transcript of OSCON - ES6 metaprogramming unleashed

ES6 metaprogramming unleashed

Javascript metaprogramming?

Metaprogramming is powerful and fun, but remember:

"With great power comes great responsibility"

about me

Javier Arias, senior software engineer at Telefonica. Technology and software development lover.

@javier_arilos

http://about.me/javier.arilos

the challenge

www.pollingdeck.com

metaprogramming

“The key property of metaprograms is that

manipulate other programs or program

representations.” - Gregor Kiczales

Meta level vs Base level

metaprogramming in real life

Compiler/transpilers: gcc, coffeescript…

Macro languages (eg. C preprocessor)

Using "eval" to execute a string as code

Database scaffolding/ORM: mongoose, …

IDEs: Eclipse, …

reflective metaprogramming

A program that metaprograms itself -

This is the subject of the talk!

Reflective Metaprogramming in JS

MMM… very interesting … is there any JS?

do you use metaprogramming?

www.pollingdeck.com

JS metaprogramming up to ES5object metaprogramming API

- Good

function metaprogramming

- Ugly

eval

- Bad

The Good: Object metapgrming API● modify property access:

○ getters & setters

○ property descriptors

● Object mutability:

preventExtensions,

seal, freeze

Obj metaprogramming: Test Spy

Test Spy myFunction

[1] myFunction = spy (myFunction)

[5] assert eg. calledOnce

[2] myFunction(1, ‘a’)

Test spy is a function that records calls to a spied function - SinonJS

[3] store call [4] call

Obj metaprogramming: Test Spy

function functionSpy(func){

Obj metaprogramming: Test Spy

function functionSpy(func){

Object.defineProperty(proxyFunc, "_callCount", {value: 0, writable: true});

Object.defineProperty(proxyFunc, "once", {get: function(){return this._callCount==1});

Obj metaprogramming: Test Spy

function functionSpy(func){

Object.defineProperty(proxyFunc, "_callCount", {value: 0, writable: true});

Object.defineProperty(proxyFunc, "once", {get: function(){return this._callCount==1});

var proxyFunc = function () { //intercept and count calls to func

proxyFunc._callCount += 1;

return func.apply(null, arguments);

};

return proxyFunc;

}

The Bad: eval

www.pollingdeck.com

The Bad: eval

avoid using eval in the browser for input from the user or your

remote servers (XSS and man-in-the-middle)

“is sometimes necessary, but in most cases it

indicates the presence of extremely bad coding.”

- Douglas Crockford

The Ugly: func metaprogramming

> Function constructor

> Function reflection

var remainder = new Function('a', 'b', 'return a % b;');

remainder(5, 2); // 1

function constructor

Create functions from Strings…

Similar to eval but differences in scope.

function reflection - length

Function.length: number of parameters of a

function.

Usage example: Express checking middlewares signature

function parameters: reflection

Get informaton about function parameters

Eg: Dependency Injection

function parameters reflection

How do we do that in JS?

Function.toString() + RegExp

This is ugly!

Function.prototype.toString

Defined in ES5 and ES2015 specs.

function getParameters(func) { //The regex is from Angular

var FN_PARAMS = /^function\s*[^\(]*\(\s*([^\)]*)\)/m;

var params = func.toString().match(FN_PARAMS)[1].split(',');

return params;

}

function parameters reflection

ES6 - The Zen of JS?

www.pollingdeck.com

ES6 and Proxy

The Proxy can define custom behavior for

fundamental operations.

➔ property lookup

➔ assignment

➔ enumeration

➔ (...)

Proxy explained

handler: interceptor. traps per operation.

proxy &

handlertarget

A Proxy wraps a target object.

target: proxied object.

proxy sample: noSuchPropertyze

var myObj = {

a: 1,

b: 'nice'

};

myObj = noSuchPropertyze(myObj); // We want to INTERCEPT access to properties (get)

myObj.b; // nice

myObj.nonExistingProperty; // Error

function noSuchPropertyze(obj) {

var handler = {

get: function(target, name, receiver){

if(name in target) return target[name];

throw new Error('Not found:' + name);

}

};

return new Proxy(obj, handler);

}

var myObj = noSuchPropertyze({a: 1, b: 'nice'});

myObj.b; // nice

myObj.nonExistingProperty; // Error

proxy sample: noSuchPropertyze

proxy &

handler

target

myObj[name]

function noSuchPropertyze(obj) {

var handler = {

get: function(target, name, receiver){

if(name in target) return target[name];

throw new Error('Not found:' + name);

}

};

return new Proxy(obj, handler);

}

var myObj = noSuchPropertyze({a: 1, b: 'nice'});

myObj.b; // nice

myObj.nonExistingProperty; // Error

proxy sample: noSuchPropertyze

proxy &

handler

target

myObj[name]

function noSuchPropertyze(obj) {

var handler = {

get: function(target, name, receiver){

if(name in target) return target[name];

throw new Error('Not found:' + name);

}

};

return new Proxy(obj, handler);

}

var myObj = noSuchPropertyze({a: 1, b: 'nice'});

myObj.b; // nice

myObj.nonExistingProperty; // Error

proxy sample: noSuchPropertyze

proxy &

handler

target

myObj[name]

Proxy usages

virtual objects: persistent or remote objects

do fancy things such as DSLs

DSL with Proxies

to(3).double.pow.get // 36

Overriding the dot (.) operator!!!

DSL with Proxies- implementation

// ==== to(3).double.pow.get ===

var to = (function closure() { // closure for containing our context

var functionsProvider = { //Object containing our functions

double: function (n) { return n*2 },

pow: function (n) { return n*n }

};

return function toImplementation(value) { // Magic happens here!

// (...) implementation

return new Proxy(functionsProvider, handler);

}

}());

DSL with Proxies- implementation

// ==== to(3).double.pow.get ===

var to = (function closure() { // closure for containing our context

var functionsProvider = { //Object containing our functions

double: function (n) { return n*2 },

pow: function (n) { return n*n }

};

return function toImplementation(value) { // Magic happens here!

// (...) implementation

return new Proxy(functionsProvider, handler);

}

}());

// ==== to(3).double.pow.get ===

var to = (function closure() { // closure for containing our context

var functionsProvider = { //Object containing our functions

double: function (n) { return n*2 },

pow: function (n) { return n*n }

};

return function toImplementation(value) { // Magic happens here!

// (...) implementation

return new Proxy(functionsProvider, handler);

}

}());

DSL with Proxies- implementation

DSL with Proxies- implementation // ==== to(3).double.pow.get === return function toImplementation(value) {

var pipe = []; //stores functions to be called

var handler =

{ get(target, fnName, receiver) {

if (fnName in target){ //eg. .double : get the function and push it

pipe.push(target[fnName]);

return receiver;} //receiver is our Proxy object: to(3)

if (fnName == "get")

return pipe.reduce(function (val, fn) { return fn(val) }, value);

throw Error('Method: '+ fnName +' not yet supported');

}, set(target, fnName, fn, receiver) {

target[fnName] = fn;} //dynamic declaration of functions

};

return new Proxy(functionsProvider, handler);}}());

DSL with Proxies- implementation // ==== to(3).double.pow.get === return function toImplementation(value) {

var pipe = []; //stores functions to be called

var handler =

{ get(target, fnName, receiver) {

if (fnName in target){ //eg. .double : get the function and push it

pipe.push(target[fnName]);

return receiver;} //receiver is our Proxy object: to(3)

if (fnName == "get")

return pipe.reduce(function (val, fn) { return fn(val) }, value);

throw Error('Method: '+ fnName +' not yet supported');

}, set(target, fnName, fn, receiver) {

target[fnName] = fn;} //dynamic declaration of functions

};

return new Proxy(functionsProvider, handler);}}());

DSL with Proxies- implementation // ==== to(3).double.pow.get === return function toImplementation(value) {

var pipe = []; //stores functions to be called

var handler =

{ get(target, fnName, receiver) {

if (fnName in target){ //eg. .double : get the function and push it

pipe.push(target[fnName]);

return receiver;} //receiver is our Proxy object: to(3)

if (fnName == "get")

return pipe.reduce(function (val, fn) { return fn(val) }, value);

throw Error('Method: '+ fnName +' not yet supported');

}, set(target, fnName, fn, receiver) {

target[fnName] = fn;} //dynamic declaration of functions

};

return new Proxy(functionsProvider, handler);}}());

DSL with Proxies- implementation // ==== to(3).double.pow.get === return function toImplementation(value) {

var pipe = []; //stores functions to be called

var handler =

{ get(target, fnName, receiver) {

if (fnName in target){ //eg. .double : get the function and push it

pipe.push(target[fnName]);

return receiver;} //receiver is our Proxy object: to(3)

if (fnName == "get")

return pipe.reduce(function (val, fn) { return fn(val) }, value);

throw Error('Method: '+ fnName +' not yet supported');

}, set(target, fnName, fn, receiver) {

target[fnName] = fn;} //dynamic declaration of functions

};

return new Proxy(functionsProvider, handler);}}());

DSL with Proxies- implementation // ==== to(3).double.pow.get === return function toImplementation(value) {

var pipe = []; //stores functions to be called

var handler =

{ get(target, fnName, receiver) {

if (fnName in target){ //eg. .double : get the function and push it

pipe.push(target[fnName]);

return receiver;} //receiver is our Proxy object: to(3)

if (fnName == "get")

return pipe.reduce(function (val, fn) { return fn(val) }, value);

throw Error('Method: '+ fnName +' not yet supported');

}, set(target, fnName, fn, receiver) {

target[fnName] = fn;} //dynamic declaration of functions

};

return new Proxy(functionsProvider, handler);}}());

Composition with Object.assign

Composition: model/create Objects by

“what they do”

const barker = (state) => ({ //factory function barker

bark: () => console.log('woof, woof ' + state.name)

})

const angryHuman = (name) => { //factory function angryHuman

let state = {name}; //state object stays in the closure

return Object.assign( //assign to {} all own properties of barker(state)

{},

barker(state),

talker(state)

)

}

var angryJavi = angryHuman('javi')

angryJavi.bark() //woof, woof javi

Composition with Object.assign

const barker = (state) => ({ //factory function barker

bark: () => console.log('woof, woof ' + state.name)

})

const angryHuman = (name) => { //factory function angryHuman

let state = {name}; //state object stays in the closure

return Object.assign( //assign to {} all own properties of barker(state)

{},

barker(state),

talker(state)

)

}

var angryJavi = angryHuman('javi')

angryJavi.bark() //woof, woof javi

Composition with Object.assign

const barker = (state) => ({ //factory function barker

bark: () => console.log('woof, woof ' + state.name)

})

const angryHuman = (name) => { //factory function angryHuman

let state = {name}; //state object stays in the closure

return Object.assign( //assign to {} all own properties of barker(state)

{},

barker(state),

talker(state)

)

}

var angryJavi = angryHuman('javi')

angryJavi.bark() //woof, woof javi

Composition with Object.assign

That’s all folks!

No animals were harmed in the preparation of this presentation.

references● Alex Rauschmayer on Proxies: http://www.2ality.com/2014/12/es6-proxies.html

● About quines: http://c2.com/cgi/wiki?QuineProgram

● Kiczales on metaprogramming and AOP: http://www.drdobbs.com/its-not-metaprogramming/184415220

● Brendan Eich. Proxies are awesome: http://www.slideshare.net/BrendanEich/metaprog-5303821

● eval() isn’t evil, just misunderstood: http://www.nczonline.net/blog/2013/06/25/eval-isnt-evil-

just-misunderstood/

● On DI: http://krasimirtsonev.com/blog/article/Dependency-injection-in-JavaScript

● Express middlewares: http://expressjs.com/guide/using-middleware.html

● Proxies by Daniel Zautner: http://www.dzautner.com/meta-programming-javascript-using-proxies/

Media● Storm by Kelly Delay: https://flic.kr/p/seaiyf

● The complete explorer: https://www.flickr.com/photos/nlscotland/

● Record Player by Andressa Rodrigues: http://pixabay.com/en/users/AndressaRodrigues-40306/

● Wall by Nicole Köhler: http://pixabay.com/en/users/Nikiko-268709/

● Remote by Solart: https://pixabay.com/en/users/solart-621401/

● Rocket launch by Space-X: https://pixabay.com/en/users/SpaceX-Imagery-885857/

● Coffee by Skeeze: https://pixabay.com/en/users/skeeze-272447/

● Sleeping Monkey by Mhy: https://pixabay.com/en/users/Mhy-333962/

● Funny Monkey by WikiImages: https://pixabay.com/en/users/WikiImages-1897

● Lemur by ddouk: https://pixabay.com/en/users/ddouk-607002/

● Fire in the sky by NASA: https://flic.kr/p/pznCk1

complete code examples

function sayHi(name){ console.log('Hi '+name+'!') }// we define a very interesting function

sayHi = functionSpy(sayHi);// now we Spy on sayHi function.

console.log('calledOnce?', sayHi.once); // false. Note that ‘once’ looks like a property!!

sayHi('Gregor'); // calling our Function!!

console.log('calledOnce?', sayHi.once); // true

function functionSpy(func){

var proxyFunc = function () { //intercept and count calls to func

proxyFunc._callCount += 1;

return func.apply(null, arguments);

};

Object.defineProperty(proxyFunc, "_callCount", {value: 0, writable: true});

Object.defineProperty(proxyFunc, "once", {get: function(){return this._callCount==1});

return proxyFunc;

}

Test Spy

function constructor vs eval

function functionCreate(aParam) { //Func consctructor cannot access to the closure

var funcAccessToClosure = Function('a', 'b', 'return a + b + aParam');

return funcAccessToClosure(1, 2);

}

functionCreate(3); //ReferenceError: aParam is not defined

function functionInEval(aParam) {//has access to the closure

eval("function funcAccessToClosure(a, b){return a + b + aParam}");

return funcAccessToClosure(1, 2);

}

functionInEval(3); // 6

var aParam = 62; //Now, define aParam.

functionCreate(3); // 65

functionInEval(3); // 6

DI container

● Function reflection (parameters) eg: Dependency Injection

var Injector = {dependencies: {},

add : function(qualifier, obj){

this.dependencies[qualifier] = obj;},

get : function(func){

var obj = Object.create(func.prototype);

func.apply(obj, this.resolveDependencies(func));

return obj;},

resolveDependencies : function(func) {

var args = this.getParameters(func);

var dependencies = [];

for ( var i = 0; i < args.length; i++) {

dependencies.push(this.dependencies[args[i]]);}

return dependencies;},

getParameters : function(func) {//This regex is from require.js

var FN_ARGS = /^function\s*[^\(]*\(\s*([^\)]*)\)/m;

var args = func.toString().match(FN_ARGS)[1].split(',');

return args;}};

var aFancyLogger = {

log: function(log){console.log(Date().toString()+" => "+ log);}

};

var ItemController = function(logger){

this.logger = logger;

this.doSomething = function(item){this.logger.log("Item["+item.id+"] handled!");};

};

Injector.add("logger", aFancyLogger); //register logger into DI container

var itemController = Injector.get(ItemController); //get Item controller from DI

itemController.doSomething({id : 5});

proxy sample: DRY REST Client// DRY REST client

function prepareGetter(resource) {

return function resourceGetter(id) {

console.log('HTTP GET /server/'+resource+( id ? '/'+id : ''));

return 200;

}

}

let proto = new Proxy({}, {

get(target, name, receiver) {

if(name.startsWith('get')) {

return prepareGetter(name.slice(3).toLowerCase());}

return target[name];

}

});

let myRestClient = Object.create(proto); //Prototype is a Proxy

myRestClient.allo = 7;

myRestClient.getClient('kent.beck'); //200 "HTTP GET /server/client/kent.beck"

myRestClient.allo; // 7;

DSL with Proxiesvar to = (function closure() {

var functionsProvider = {

double: function (n) { return n*2 },

pow: function (n) { return n*n }

};

return function toImplementation(value) {

var pipe = [];

var handler =

{

get(target, fnName, receiver) {

if (fnName == "get")

return pipe.reduce(function (val, fn) { return fn(val) }, value);

if (fnName in target) {

pipe.push(target[fnName]);

return receiver;}

throw Error('Method: '+ fnName +' not yet supported');

},

set(target, fnName, fn, receiver) {

target[fnName] = fn;} //dynamic declaration of functions

};

return new Proxy(functionsProvider, handler);}}());

console.log('to(3).double.pow.get::',to(3).double.pow.get); // 36

console.log('to(2).triple::', to(2).triple.get); //Error: Method: triple not yet supported

to().triple = function(n) {return n*3};

console.log('to(2).triple::',to(2).triple.get);

Composition with Object.assignconst barker = (state) => ({ //factory function barker

bark: () => console.log('woof, woof ' + state.name)

})

const angryHuman = (name) => { //factory function angryHuman

let state = {name}; //state object stays in the closure

return Object.assign( //assign to {} all own properties of barker(state)

{},

barker(state)

)

}

var angryJavi = angryHuman('javi')

angryJavi.bark() //woof, woof javi

proposal

Description

ES6 delivers some exciting metaprogramming capabilities with its new Proxies feature.

Metaprogramming is powerful, but remember: "With great power comes great responsibility". In the

talk we will shortly revisit Javascript metaprogramming and explain ES6 Proxies with code

examples.

Session type: 40-minute session

Topics: none

Abstract

During the talk we will explain different metaprogramming concepts and techniques with code

samples related to a very simple testing library.

First, we will discuss about what metaprogramming is, what metaprogramming is useful for and a

very light overview of metaprogramming in other programming languages.

Current capacities from Javascript up to ES5 will be revisited with some code examples.

Finally, ES6 and proxies will be covered.