Saturday, 31 August 2013

Implement universal, lightweight, and unobtrusive tagging of arbitrary objects?

Implement universal, lightweight, and unobtrusive tagging of arbitrary
objects?

I'd like to implement a universal, lightweight, and "unobtrusive" way to
"tag" arbitrary objects.
More specifically, I want to define the equivalent of the (abstract)
functions tag, isTagged, and getTagged, such that:
isTagged(t) is true if and only if t was the value returned by tag(o), for
some object o;
getTagged(tag(o)) is identical to o, for every object o;
if t = tag(o), then tag(t) should be identical to t;
with the exception of the behaviors described in (1), (2), and (3) above,
and strict identity tests involving ===, tag(o) and o should behave the
same way.
For example:
>>> isTagged(o = "foo")
false
>>> isTagged(t = tag(o))
true
>>> getTagged(t) === o
true
>>> tag(t) === t
true
>>> t.length
3
>>> t.toUpperCase()
"FOO"
Below I give my best shot at solving this problem. It is (almost)
universal, but, as it will soon be clear, it is anything but
lightweight!!! (Also, it falls rather short of fully satisfying
requirement 4 above, so it is not as "unobtrusive" as I'd like. Moreover,
I have serious doubts as to its "semantic correctness".)
This solution consists of wrapping the object o to be tagged with a "proxy
object" p, and copying all the properties of o (whether "owned" or
"inherited") to p.
My question is:
is it possible to achieve the specifications given above without having to
copy all the properties of the tagged object?



Here's the implementation alluded to above. It relies on the utility
function getProperties, whose definition (FWIW) is given at the very end.
function Proxy (o) { this.__obj = o }
function isTagged(t) {
return t instanceof Proxy;
}
function getTagged(t) {
return t.__obj;
}
var tag = (function () {
function _proxy_property(o, pr) {
return (typeof pr === "function")
? function () { return pr.apply(o, arguments) }
: pr;
}
return function (o) {
if (isTagged(o)) return o;
if (typeof o.__obj !== "undefined") {
throw TypeError('object cannot be proxied ' +
'(already has an "__obj" property)');
}
var proxy = new Proxy(o);
var props = getProperties(o); // definition of getProperties given below
for (var i = 0; i < props.length; ++i) {
proxy[props[i]] = _proxy_property(o, o[props[i]]);
}
return proxy;
}
})();
This approach, ham-fisted though it is, at least seems to work:
// requirement 1
>>> isTagged(o = "foo")
false
>>> isTagged(p = tag(o))
true
// requirement 2
>>> getTagged(p) === o
true
// requirement 3
>>> tag(p) === p
true
// requirement 4
>>> p.length
3
>>> p.toUpperCase()
"FOO"
...well, almost; requirement (4) is not always satisfied:
>>> o == "foo"
true
>>> p == "foo"
false
>>> o == o
true
>>> p == o
false



FWIW, here's the definition of the function getProperties, which is used
by the tag function. Criticisms welcome. (WARNING: I'm a completely
clueless JS noob who doesn't know what he's doing! Use this function at
your own risk!)
function getProperties(o) {
var seen = {};
function _properties(obj) {
var ret = [];
if (obj === null) {
return ret;
}
try {
var ps = Object.getOwnPropertyNames(obj);
}
catch (e if e instanceof TypeError &&
e.message === "obj is not an object") {
return _properties(obj.constructor);
}
for (var i = 0; i < ps.length; ++i) {
if (typeof seen[ps[i]] === "undefined") {
ret.push(ps[i]);
seen[ps[i]] = true;
}
}
return ret.concat(_properties(Object.getPrototypeOf(obj)));
}
return _properties(o);
}

No comments:

Post a Comment