Wednesday 23 July 2014

Order Preserving Object

Some weeks ago one coleague showed me a pretty cool perl library, Tie, that among other things allows to preserve the order in which items are added to a Hash (meaning that you can iterate the Hash in that order)

use Tie::IxHash;
my $items = {};
tie (%{$items}, 'Tie::IxHash');

Given the similarities between Perl and JavaScript I began to think of a way to accomplish the same in JasvaScript. Notice that JavaScript does not guarantee that the order in which a "for in" loop traverses an object (or Object.keys returns its keys) is the same in which those properties were inserted, the specification says it quite clearly. So far, it seems like many JavaScript engines preserve that order, but I guess this is just an effect of how they lay out objects in memory, it's an implementation detail on which we can not rely.

Achieving this feature in ES6 does not seem particularly complicated thanks to proxies. Notice that I'm using Direct Proxies, and at the time of this writing only Firefox seems to implement them, so I'm trying to make this code work just in Firefox (no node.js fun this time). Basically, we could create an OrderPreservingObject, by creating a proxy object around a plain JavaScript object and trapping the set and delete actions, so that it adds the key to an array that holds the order of that object.

function OrderPreservingObject(initObj){
	var obj = {
		orderedKeys: [],
	};
	//to prevent orderedKeys from showing up when iterating with "for in" we should declare it as non enumerable, 
	//but anyway, we should not use "for in" for iterating this object
	//ideally we would have managed to trap the "for in" and we would exclude it directly there
	var proxy = Proxy(obj, {
		
		//receiver is the proxy itself
		set: function(target,name,val,receiver){    
			//console.log("set invoked");
			target[name] = val;
			if (target.orderedKeys.findIndex(function(it){
				return it == name;
			}) == -1){
				target.orderedKeys.push(name);
			}
			return true;
		},
		deleteProperty: function(target,name){
			//console.log("delete invoked");
			delete target[name];
			var index = target.orderedKeys.findIndex(function(it){
				return it == name;
			});
			if (index != -1){
				target.orderedKeys.splice(index, 1);
			}
			return true;
		}, 
				
		keys: function(target){
			//console.log("keys invoked");
			return target.orderedKeys;
		}
	});
	//now that the proxy is set up, we can add the items in the initObj and they'll be added in order
	if (initObj){
		for (var key in initObj){
			proxy[key] = initObj[key];
		}
	}	
	return proxy;
}

Once we have the pieces to keep the order, the main problem is, how should we do to iterate the object following that order? The ideal solution would be iterating the object with "for in". "for in" iteration logic does not depend on our objects implementing a certain iteration method, so it can not be altered or hinted by users. Well, that's true for normal objects, but looking into the ES6 documentation, we see that in principle proxies are also able modify the "for in" behavior. So, I came up with this method/trap:

enumerate: function* (target){
	console.log("enumerate invoked");
	for (var i=0; i<target.orderedKeys.length; i++){
		yield target.orderedKeys[i];
	}
}

But I guess I'm missing something, cause the above does not trap the "for in". The "for in" runs as normal, iterating the object without taking into account our trap function (it iterates the object in order, but not based on our ordering extra logic, just cause for the moment Firefox's internal implementation of objects and "for in" happens to preserve the order). After a fast search I haven't found any information about how this "enumerate" trap is supposed to work, so I guess it'll have to wait.

In C# and Java iterating an object with foreach works just by obtaining an Enumerator/Iterator for that object and working with it. ES6 implements iterators, and while the "for in" loop will continue to work right the same as it does now, the new "for of" loop has been added to work with iterators and their next method. So well, maybe the best way to iterate my OrderPreservingObject would be to make it implement the iterator protocol and use it with a "for of" loop.

		__iterator__: function* (){
			for (var i=0; i<this.orderedKeys.length; i++){
				yield this.orderedKeys[i];
			}
		}

Unfortunately, the above does not work either. Trying to use it will fail in Firefox:
TypeError: dic1['@@iterator'] is not a function

So it seems like I'm left with no other option but iterating my object with a normal for loop, either using Object.keys

for (var i=0; i<Object.keys(dic1).length; i++){
	var key = Object.keys(dic1)[i];
	console.info(key + " - " + dic1[key]);
}

or directly using my orderedKeys array (that otherwise I would prefer to keep out of the Object interface)

for (var i = 0; i< dic1.orderedKeys.length; i++){
	var key = dic1.orderedKeys[i];
	console.info(key + " - " + dic1[key]);
}

Both solutions are quite verbose, so in the end, I came up with the idea of adding a forEach method (similar to the one in Array.prototype) to my Object:

forEach: function(actionFunc){
	//where actionFunc receives: item, key, tiedDic
	for(var i=0; i<this.orderedKeys.length; i++){
		var key = this.orderedKeys[i];
		actionFunc(this[key], key, this);
	}
}

so that it can be iterated like this:

dic1.forEach(function(item, key){
	console.info(key + " - " + item);
});

You can find the whole code here.

Definitely, I should revisit this in a few months as the adoption of ES6 evolves, to see if I finally make the "for in" trap or the "for of"/__iterator__ work.

No comments:

Post a Comment