Combining Mirror Facets

April 29, 2011

in JavaScript,Reflection

In my last couple posts I introduced idea of using Mirrors for JavaScript reflection and took a first look at the introspection interfaces of my jsmirrors prototype. In this post I’m going to look at the other reflection interfaces in jsmirrors and how they are mixed together to provide various levels of reflection privilege.

When building this prototype I knew that I wanted to have a number of separable sets of reflection capabilities that I could mix and match in various ways. I also knew that the implementation was likely to change several times as I experimented with the prototype. I wanted to make sure that as I evolved the implementation that I could keep track of what belonged in each separable piece. The way I ultimately accomplished this was by maintaining a file of interface definitions that are separate from the actual code that implements jsmirrors. The interface specifications are contained in the file mirrorsInterfaceSpec.js. I look at the interface file when I need to remind myself how to use one of the specific reflection interfaces and as a specification as I make changes to the implementaiton. Also, whenever I perform a major refactoring of the implementation I check it against the interface specification. Here is the interface specification of the basic object introspection interface that I demonstrated in the Looking into Mirrors post:

//Mirror for introspect upon all objects
var objectMirrorInterface = extendsInterface(objectBasicMirrorInterface, {
   prototype:  getAccess(objectMirrorInterface|null),
     //return a mirror on the reflected object's [[Prototype]]
   extensible: getAccess(Boolean),
     //return true if the reflected object is extensible
   ownProperties: getAccess(array(propertyMirrorInterface)),
     //return an array containing property mirrors
     //on the reflected object's own properties
   ownPropertyNames: getAccess(array(String)),
     //return an array containing the string names
     //of the reflected object's own properties
   keys: getAccess(array(String)),
     //return an array containing the string names of the
     //reflected object's enumerable own properties
   enumerationOrder: getAccess(array(String)),
     //return an array containing the string names of the
     //reflected object's enumerable own and inherited properties
   prop: method({name:String}, returns(propertyMirrorInterface|undefined)),
     //return a mirror on an own property
   lookup: method({name:String},returns(propertyMirrorInterface|undefined)),
     //return mirror on the result of a property lookup. It may be inherited 
   has: method({name:String}, returns(Boolean)),
     //return true if the reflected object has a property named 'name'
   hasOwn: method({name:String}, returns(Boolean)),
     //return true if the reflected object has an own property named 'name'
   specialClass: getAccess(String)
    //return the value of the reflected object's [[Class]] internal property
});

I used JavaScript object literals and a few helper functions to describe these interfaces. Here is the definition of the helper functions used for this interface:

function getAccess(returnInterface) {}; //a "get-able" property
function method(arguments,returnInterface){}; // a method property
function extendsInterface(supers,members) {};//a interface adding to supers
function returns(returnInterface) {};   //return value of a method
function array(elementInterface) {};//array elements all support a interface

The JavaScript code of the interface definitions don’t actually do anything but I find that being able to parse the interface specification using JavaScript forces me to apply some useful structuring discipline that I might skip if I was just writing prose descriptions. Plus I think it is going to be quite useful to have these interface specifications in a form that is easily processed. For example, now that I have an initial implementation of jsmirrors, I may use it to create a little tool that can reflect upon the objects created by the interface specifications and perform useful tasks. For example I may generate unit test stubs for implementations of the interfaces. I may also use reflection over the interfaces to directly validate the completeness of my implementations.

In factoring the jsmirrors functionality for reflecting upon objects I divided it to three primary interfaces. objectMirrorInterface, shown above, is the basic introspection interface. objectMutationMirrorInterface allows changes to be made to a reflected object such as adding or removing properties or changing the object’s prototype. objectEvalMirrorInterface allows various forms of evaluation upon reflected objects such as doing “puts” and “gets” (which may invoke accessor property functions) to access property values of a reflected object or to invoke a method property. There are also corresponding introspection, mutation, and evaluation interfaces for function object mirrors and also for property mirrors.

In the actual implementation, these interfaces are combined in various ways to produce five different kinds of concrete mirrors on local objects. These various kinds of mirrors are accessible via factory functions that are accessed as properties of the Mirrors module object. The five local object mirror factories are:

  • Mirrors.introspect – supports only introspection using objectMirrorInterface.
  • Mirrors.evaluation – supports only evaluation using objectEvalMirrorInterface.
  • Mirrors.introspectEval – supports introspection and evaluation using objectMirrorInterface and objectEvalMirrorInterface.
  • Mirrors.mutate – supports introspection and mutation using objectMirrorInterface and objectMutationMirrorInterface.
  • Mirrors.fullLocal – supports introspection, mutation, and evaluation using all three interfaces.

I demonstrated the use of Mirrors.introspect is my previous post. The other Mirror factories are used in exactly the same manner and, except for Mirrors.evaluation, could be used to run all the same examples. However, the other factories expose additional functionality that isn’t available using Mirrors.introspect. Take a look at the actual interface specification in mirrorsInterfaceSpec.js to see which capabilities are provided by the mirror objects produced by each of these factories.

The reason for providing multiple mirror factories is to demonstrate that by using mirror-based reflection we can decide exactly how much reflection capability we will make available to any specific client or tool. We might allow one tool to use the full range of reflective interfaces. For another we may only expose introspection or evaluation capabilities or perhaps introspection and mutation capabilities without the ability to actually do reflective evaluation. However, so far, I’ve only shown mirrors that know how to reflect upon local objects that exist in the same heap as the mirror objects. In my next post I’ll look at how to use the same interfaces to reflect upon non-local objects that might be encoded in a file or exist in a remote environment.

(Photo by “Metro Centric”, Creative Commons Attribution License)

{ 2 comments }

Gilad Bracha May 30, 2011 at 9:38 am

Hi Allen,

Great stuff! And thanks for citing us. How do you see this work affecting ES? Also, since JS allows you to reflect directly, you need a lot of discipline to stick to using your mirrors, and cannot guarantee that they are not being bypassed. Any thoughts on that?

allen May 30, 2011 at 2:02 pm

Thanks, Gilad, Great questions. I actually have a post planned about exactly these sorts of issues. I expect to get back to it real soon.

In brief, there are still plenty of aspect of JS that aren’t accessible via reflection and there are new features being added that should have reflection support. Rather than adding more ad hoc reflection APIs, mirrors seem like a better way to go forward. We’ll have to see how it plays out over the long term.

Allen

Previous post:

Next post: