48
ES6 metaprogramming unleashed

ES6 metaprogramming unleashed

Embed Size (px)

Citation preview

ES6 metaprogramming unleashed

are we ready?

Metaprogramming is powerful and

fun, but remember:

"With great power comes

great responsibility"

about me

Javier Arias Losada, senior software engineer at Telefonica.

Currently working at EyeOS, helping to create an VDI (Virtual Desktop Infrastructure) platform.

@javier_arilos

http://about.me/javier.arilos

metaprogramming

“The key property of metaprograms is that

manipulate other programs or program

representations.”

- Gregor Kiczales

metaprogramming levels

Meta level:

program manipulating other program

Base level:

program manipulated

metaprogramming samples

Compiler/transpilers: gcc, coffeescript…

Macro languages (eg. C preprocessor)

Using "eval" to execute a string as code

Database scaffolding/ORM: mongoose, …

IDEs: Webstorm, …

reflective metaprogramming

A program that modifies itself -

Reflective Metaprogramming in JS-

This is the subject of the talk!

MMM… very interesting … but when are we going to JS?

Introspection

Introspection: read the structure of a program.

Object.keys()

var obj = { //Base level

name: 'Santa',

hello: function() {

console.log('Hello', this.name,'!');

}

};

Object.keys(obj).forEach(function(prop) {

console.log(prop); //Meta level

});

Self-modification

Self-modification: change program structure.

function renameProperty(obj, oldName, newName) {

obj[newName] = obj[oldName];

delete obj[oldName];

}

renameProperty(obj, 'hello', 'greet');

Intercession

Intercession: redefine semantics of some language operations.

Object.defineProperty(obj, "roProp", {

value: 101,

writable: false,

enumerable: true});

obj.roProp = 102

obj.roProp //101

‘Cheating’ quine:

(function a(){console.log('('+a+')()')})()

‘Cheating’ quine:

(function a(){console.log('('+a+')()')})()

A real one:

_='"'+";document.write('_=',_[17],_[0],_[17],'+',_,_)";document.write('_=',_[17],_

[0],_[17],'+',_,_)

metaprogramming fun: Quines

Quine: a program that prints a copy of its own

source code (no input allowed)

Caffeine Facts:

A cup of coffee works just 10

minutes after you drink it.

It takes 45 minutes for 99% of

caffeine to be absorbed.

Please… awake people

around you!!

JS metaprogramming up to ES5THE GOOD:object metaprogramming API

THE UGLY:function metaprogramming

THE BAD:eval

The Good: Object metapgrming API● modify property access:

○ getters & setters

○ property descriptors

● Object mutability:

preventExtensions,

seal, freeze

Sample: 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

// we define a very interesting function

function sayHi(name){ console.log('Hi '+name+'!') }

// now we Spy on sayHi function.

sayHi = functionSpy(sayHi);

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

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

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

Sample: Test Spy

accessor properties sample - Spy

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;

}

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:

○ Function.prototype.length

○ Ugly hacks

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

remainder(5, 2); // 1

function constructor

Seems similar to eval but…

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

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

function reflection - length

Function.length returns the number of parameters

of a function.

Usage example: Express checking middlewares signature

function parameters reflection

We want to get informaton about function

parameters.

Parameters belong to function signature.

Arguments are passed to a function call.

function parameters reflection

Is it a bad joke?

Function.toString() + RegExp

to the rescue!

How do we do that in JS?

DI container implementation

Defined in ES5 and ES2015 specs.

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

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

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

return funcParams;

}

fucntion parameters reflectionSome libraries with Dependency Injection, such as Angular.js use this technique:

Eyes still opened?

Coffee must be working!

ES6 and Proxy

The Proxy can define custom behavior for

fundamental operations.

property lookup, assignment, enumeration, function invocation

Proxy concepts

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]

Proxy usages

Why do we need proxies?virtual objects: persistent or remote objects

emulate the dreaded "host objects"

do fancy things such as DSLs

proxy sample: DRY REST Client// REST client

let myRestClient = {

getClient: function(id) {

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

return 200;

},

getBill: function(id) {

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

return 200;

},

// (...) DO YOU SEE THE PATTERN?

}

myRestClient.allo = 7;

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

/server/client/kent.beck"

myRestClient.allo; // 7;

proxy sample: DRY REST Clientfunction 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 Proxies

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

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 === 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 - new methods

to(2).triple.get; //Error: Method: triple not yet supported

to().triple = function(n) {return n*3}; //Add new method: triple

to(2).triple.get; // 6

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/

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

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});

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);