In the previous post we explored options to express dynamic behavior of "formatting" an object for output. First option was to use relatively well fitting dynamic operation from the DLR core set - CodeRepresentation. There's another option which we'll look at now.
Using Extension Methods
Instead of using CodeRepresentation, we could 'encode' the dynamic operation of "format for output" as a call to an object's method. It is not that far fetched, actually. Python does just that with the __repr__ method.
Suppose we had this "FormatForPrint" method on each object, our ToyBinder would have very easy job ... find it and call it (create tree that will call it that is). Problem is, of course, that not every object has "FormatForPrint".
Luckily, we - the language implementers - are in complete control here, because we can modify the code in ToyBinder to maintain the correct pretense. If the object being passed in has a type we are interested in, we can simply generate appropriate call to the formatting method, similarly to what we did in the previous post, but with a significant difference.
In the last post, all "Format" methods were in the same place, regardless of the type of the object that they format. That's a good way to look at the problem if we only want to implement formatting. Once we add many other behaviors, we'd have to deal with many classes similar to Formatter to contain all such methods. We could refactor our code in a way that all helper methods relevant to a given type are in the same place. One for formatting, another for ... whatever. This way, once the language action binder sees object of a given type, it has only one place to look at to find all these helper methods.
The is very similar to what already exists in C# and VB ... extension methods. For a given type we can declare a static class which is a container for these extension methods and compiler will make them appear on the types they extend. All of a sudden, types have methods they haven't had before, something we are trying to get our ToyBinder to do.
Let's see if this can possibly work ...
First, the encoding of the dynamic operation. The "print" statement in ToyScript could transform into the following dynamic operation:
Ast.Statement(
Span,
Ast.Call(
typeof(Console).GetMethod(
"WriteLine", new Type[] { typeof(string) }
),
Ast.Action.InvokeMember(
SymbolTable.StringToId("FormatForPrint"),
typeof(string),
InvokeMemberActionFlags.None,
new CallSignature(0),
_expression.Generate(tg)
)
)
);
To print means, invoke "FormatForPrint" method on the argument, and pass the resulting string to Console.WriteLine.
If we run the code with this change, we get an exception: "There is no member 'FormatForPrint' on ... object". No surprise. We still have to modify our ToyBinder to do the work. It seems like a lot of coding though, detect the type of the argument, find the corresponding 'extension' and then look for the right method to call.
Luckily for us, DLR understands the notion of extension methods so all we need to do is to actually implement the extension methods and then tell DLR which class (or classes, there may be several of those) contain extension methods for a type. And that should be quite simple.
Extension methods
The implementation of FormatForPrint extension methods for some basic types may look like so:
public static class ObjectExtensions {
public static string FormatForPrint(object o) {
return "GENERIC: " + (o != null ? o.ToString() : "(null)");
}
}
public static class DoubleExtensions {
public static string FormatForPrint(double d) {
return "DOUBLE: " + d;
}
}
public static class DecimalExtensions {
public static string FormatForPrint(decimal d) {
return "DECIMAL: " + d;
}
}
In ToyScript, there are actually already an extension methods for string (StringExtensions) so we can just add one more method:
public static class StringExtensions {
public static string FormatForPrint(string o) {
return "STRING: " + o;
}
// other extension methods
// ...
}
The last thing to do is tell DLR about these extensions. There's place for that in the virtual method ActionBinder.GetExtensionTypes(Type). We just need to override it and provide the extensions. This happens in ToyBinder.cs:
protected override IList<Type> GetExtensionTypes(Type t) {
Type ext;
if (t == typeof(string)) {
ext = typeof(StringExtensions);
} else if (t == typeof(double)) {
ext = typeof(DoubleExtensions);
} else if (t == typeof(decimal)) {
ext = typeof(DecimalExtensions);
} else {
ext = typeof(ObjectExtensions);
}
return new Type[] { ext };
}
This is just a trivial implementation, the ultimate version would use some kind of a cache which would get populated as our binder loads assemblies with extensions, based on custom attributes. That's what Python does.
Now what will happen when we run?
-
ToyScript defines that to print an object, we must dynamically invoke "FormatForPrint" method on it and then send the resulting string to Console.WriteLine.
-
At runtime, DLR will try to figure out for each object what it means to perform the dynamic operation. Currently the "InvokeMember" means just find a member and then call it.
-
Via the DLR extension mechanism we extended the basic types with extension methods that DLR will now see and all objects now have "FormatForPrintPrint" methods on them
-
DLR already knows how to call a .NET method so our extension method will be called like any other .NET method would.
And the best part is that it does work:
Summary
The extension methods are pretty powerful mechanism that DLR has to extend existing types with additional functionality and it can certainly be used for the problem at hand (and many others).
The reason I said that there are several good ways to implement what Ales asked but no perfect way (yet) is that ultimately, the perfect solution could be if language could simply produce a custom dynamic operation for the operations that don't seem to fit too well into the pre-defined (and currently fixed) set. Beyond that, we can see that the two alternatives we explored both produce working results.
There is one last issue to solve ... so far, the placement of the dynamic operation was determined from source code. There was keyword "print" which we used as the guide as to where to place the dynamic operations. What if there was no keyword "print" (like in Python), what if it was just like any other function (like it is in JScript). What then? Where would be put the dynamic operations and where would the dynamic site live? We'll look at that problem next time.