ES6 metaprogramming unleashed
-
Upload
javier-arias-losada -
Category
Software
-
view
528 -
download
0
Transcript of 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?
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:
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- 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
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/
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);