Sneak Peek: Duck Typing and “Soft Interfaces” in Delphi Prism XE2 and Cooper
One of the new language features in the upcoming Oxygene 5 compiler is support for duck typing and what we call “soft interfaces”.
The name “duck typing” comes from the old saying that if something walks like a duck and quacks like a duck, it is a duck – and applies the same concept to objects.
Imagine we have the following (a bit contrived) types declared, and let’s further assume that some of them are outside of our direct control – maybe they are declared in the core framework, or in a piece of the project we don’t want to touch:
type IFooBar = interface method DoFoo; method DoBar; end; Foo = class method DoFoo; end; Bar = class method DoBar; end; FooBar = class method DoFoo; method DoBar; end;
As you see, we have an interface IFooBar that declares a couple of methods. We also have three classes that look pretty similar but with one caveat: while they do implement some of the same methods as IFooBar, they don’t actually implement the IFooBar interface. This means that if we now have a method like this:
method Test(o: IFooBar);
we cannot actually pass a FooBar to it, even though FooBar obviously implements all the necessary methods. There’s nothing that our Test method could throw at the FooBar instance that it could not handle, yet we can’t just pass it in.
Enter duck typing
With the upcoming Fall 2011 release of Oxygene, we’re introducing a new generic Compiler Magic Function called duck that allows you to apply duck typing to let the compiler convert a FooBar into an IFooBar, where necessary. For example, you could write:
var fb := new FooBar; Test(duck<IFooBar>(fb));
and pass the object in. The result of duck is, essentially, an IFooBar, and you can use it in any context that accepts an IFooBar – method calls, variable assignments, you name it.
Once again, this works because FooBar implements all the necessary methods to satisfy an IFooBar implementation.
So what if that is not the case? What would happen if instead we write the following?
var f := new Foo; Test(duck<IFooBar>(f));
As you remember from above, Foo implements DoFoo, but not DoBar, so clearly it doesn’t qualify to be duck typed as an IFooBar? That’s correct, and in fact the line above would fail, with an error such as:
(E265) Static duck typing failed because of missing methods9 (N2) Matching method "MyApplication.IFooBar.DoBar" is missing
But what if you’re fully aware your object only satisfies a subset of the interface, and you want to pass it anyway? Maybe you know that Test only makes use of DoFoo, but not of DoBar?
Oxygene’s duck typing has a solution for you there as well, by passing an optional DuckTypingMode enum value to the duck() function. DuckTypingMode has three values; the default is Static, and you’ve seen it in action above. Static duck typing will enforce that the passed object fully qualifies for the interface, and will fail with a compiler error if any member (method, property or event) of the interface is not provided by the type.
The second DuckTypingMode is Weak. In weak mode, the compiler will match any interface members it can find, just like in static mode. But for any member it does not find on the original type, it will generate a stub that throws an Not Implemented exception. This enables you to write:
var f := new Foo; Test(duck<IFooBar>(f, DuckTypingMode.Weak));
and successfully pass a Foo object to Test(). As long as test only calls DoFoo, everything will be fine and work as expected; if Test were to call DoBar, the Not Implemented exception would be thrown, at runtime.
The third and final DuckTypingMode is Dynamic. Dynamic duck typing will not directly map methods of the source object to the interface; instead, it will create a wrapper class that will dynamically call the interface members, based on what is available at runtime.
So this is all good and well, but imagine you have a large (and untouchable) library with classes that implement the DoFoo/DoBar pattern, and you plan to use those all over your code base. Sure, you can declare IFooBar, and use the duck method to duck-type those objects all over the place, but that will get annoying quickly. The compiler knows that DoFoo and DoBar methods are enough to satisfy your pattern, so wouldn’t it be great if you could let the compiler worry about the duck typing where necessary?
That’s where soft interfaces come in. Instead of declaring IFooBar as above, you could declare it as a soft interface, as follows:
type IFooBar = soft interface method DoFoo; method DoBar; end;
Simply adding the “soft” keyword lets the complier know that this interface represents a pattern it will find in classes that do not actually implement the interface themselves. As a result, you can now simply declare the Test method as before:
method Test(o: IFooBar);
And just pass your FooBar instances to it – no call to duck necessary.
var fb := new FooBar; Test(fb);
In essence, the compiler will treat any class that implements the matching methods – DoFoo and DoBar, in this case – as actually implementing the interface. This works even for classes imported from external frameworks.
To give a more concrete sample based on real life objects, imagine the following scenario:
type INumberToStringFormatter = soft interface method ToString(format: String): string; end; var d: Double := 15.2; var x: INumberToStringFormatter := d; console.WriteLine(x.ToString('m')); // money formatting
Different to the regular ToString() method, not every object in .NET implements ToString(String). Yet with the soft interface declared here, you now have a common type that you could assign a Double, an Int32 or even a Guid to – and call ToString(String) on. All with complete type safety.
More to Come
This is just a sneak peek at one set of new features coming this fall. Duck Typing and Soft Interfaces will be available in both Delphi Prism XE2 (a.k.a. Oxygene for .NET) and in Project Cooper, our upcoming Oxygene for Java.
Stay tuned for more.