The One True Object (Part 2)
In order to support a broad range of languages in this kind of a type system, we need to have a standard set of clearly defined messages that all objects can respond to. The set needs to be rich enough to capture all of the unique properties of the different languages that we're working with while at the same time be sufficiently common so that code written in different languages can work together. This is a balance that I'm sure we're going to need to adjust as we bring more languages to the DLR - but I think we have an excellent start and lots of success with our initial four languages. Here is our current set of operations:
- [Get|Set|Delete]Member(name, case-sensitivity) - gets, sets or deletes a named member on an object
- Call/CreateInstance(argument modifiers) - standard call or 'new' call to an object with arguments. Modifiers include parameter names, support for expanding argument list or keyword dictionaries and a marker for an argument representing an implicit this.
- SimpleOperation(OperationKind enumeration) - catch-all for simple common operations like add and subtract as well as indexing, etc.
- Convert(Type) - converts an object to a given static type if possible
Given this set of messages, we also need to have some mechanisms in place to allow objects of different types to respond to these messages. While the DLR lets developers treat objects uniformly whether they come from a static or a dynamic language, under the hood we need to use different mechanisms to implement this message passing for the two different cases.
Standard CLR static types
When an object is of a Type as defined by a statically typed language, the object's behavior is completely described by this Type. We use the runtime Type of the object to determine the correct behavior. For example, with the GetMember message, this will mean looking for a field, property or method with the given name on the object's Type and returning the approriate member for the object if found and otherwise throwing an exception.
The DLR will use all of the existing well known attributes specified in the Common Language Subset (CLS) in order to map members of a type onto a DLR operation. For example, we'll recognize add operator methods to use when responding to an add message. In addition, we will make standard objects behave as expected, for example, any delegate object will respond properly to a Call message.
In addition to these existing well-specified operators, the DLR adds some additional custom attributes that effectively extend the existing set of CLS operations to include support for overriding standard DLR messages. A good example of this is the static attribute that will let a type override member lookup to respond to a GetMember message. This lets a static type support name-based lookup when it is being called from a dynamic language. In our current DLR hosting work, we've found several excellent places to use this attribute. One good example is the FindControl method on page objects. With this attibute we can make working with pages feel more natural to a dynamic language:
page.FindControl("text1") --> page.text1
In fact, we use a slightly more complicated method to override the GetMember behavior of the ASP.NET page objects. We can't modify the actual types because we want to run on existing shipping versions of ASP.NET. This means that we need to extend these types externally. To do this we build on the new feature of extension methods that was added to C#3 and VB9 as part of the suite of features that support LINQ. Extension methods let developers define methods to be added to a type outside of the definition of that type itself. These extension methods can be imported like a standard namespace and will then behave as if they were originally defined on the types that they apply to.
The DLR uses a slightly extended version of extension methods. First, we add support for properties and operator methods. Second, we allow extension methods to be provided at other scopes than just the per-file scope used by C# and VB.NET. For example, we allow languages to choose to apply extension methods to a given type for all code written in that language. This is the way that we support the standard Python methods on core CLR types such as string so that Python programmers can call "s.title()" even though System.String has no method with that name. This same mechanism also allows us to support special Python members on objects such as "__class__" or "__getitem__" by modifying the view of the type from Python code - rather than modifying the type itself. This lets all of our languages that want to use an immutable string object share the same actual instances freely while using extension methods to customize the view of that type to be appropriate to each language.
Fully dynamic types
One very obvious thing missing from the current DLR is a standard default implementation of this interface. This would be useful to people who wanted to implement simple scripting languages on the DLR who weren't trying to precisely match the semantics of an existing language. This would also be useful for libraries that wanted to create a generic expando object to pass to consumers. I expect that we'll provide this in the future, but at the moment our attention remains focused on the cases where we need to capture the exact semantics of existing dynamic languages where the interface approach provides the needed flexibility.
Obvious things that we haven't gotten to yet
A few obvious questions at this point may be how the different languages can both use these standard messages and yet retain all of the details of their own unique semantics. The simple trick here is that while the messages are standard, each language has to decide which messages to pass for a given piece of source code. This gives sufficent flexibility - although I expect you'll want to hear more details about that. The second obvious question may be that the mechanisms as described above sound awfully slow with a lot of introspection and interface calls going on at runtime. This description tries to capture the semantics of message passing in the DLR - not the implementation. It's the DLR's job to make this run fast and we're quite confident that we do this well today and will keep doing better for many tomorrows, but there's a much bigger explanation owed here as well.
Note for those wanting to explore the source code
I'd still caution most people to wait a few months to really dive into these bits as our design is actively being refactored and documentation is still mostly non-existent. However, if you'd like to look at the code you can find the DLR bits in the IronPython 2.0alpha1 release under the Microsoft.Scripting and Microsoft.Scripting.Vestigial projects. In case it's not obvious, the one with "Vestigial" in its name isn't expected to be around for much longer. The set of standard messages described in this post are found in the Microsoft.Scripting.Actions namespace - where these "messages" are called "Actions". The interface IDynamicObject is currently misnamed. The current version of that interface will soon be removed and the oddly named interface IActionable will be renamed to IDynamicObject. For now, you should look at IActionable.