Introduction
Polymorphism is treated in this article as "the ability to interact with objects of different classes in a universal manner". Ad Hoc Adaptable (AHA) polymorphism is discussed below. The library to enable AHA-polymorphism for the C# programming language is described in details.
Background
Generally speaking, there are the following kinds of polymorphism in the languages I know:
- classical polymorphism using inheritance and virtual methods, just like in most of the statically typed object-oriented languages;
- unsafe runtime ad hoc polymorphism, just like in different script languages;
- safe compile-time polymorphism, just like in C++.
For example, suppose that you have an advanced third-party Com.Example.ListBox
control. It differs from the standard System.Windows.Forms.ListBox
by many features. The singularity of this ListBox
is that it uses a ListBox.Item
interface to receive names of items. Maybe it's because developers of this control forget about the Object.ToString()
method. Or maybe they want to introduce the Item.Color
property in the near future.
Further, you have two classes: Rocket
and Smile
. They are strongly independent. Moreover, both of the are not intended to be viewed in the ListBox
control. Rocket
is intended for flying and Smile
is intended for smiling. Both of them were designed and implemented by different teams of programmers two years ago. But now you are responsible to create a ListBox
with the rockets and smiles alternately for some business reasons.
By a fluke, both classes already have the Name
property.
The most naive way to solve this problem is to inherit these classes from the ListBox.Item
interface. The main drawback is the unwanted connectivity that will be produced. Remember that our classes initially aren't intended to be listed in the ListBox
at all.
The C# code for this solution is shown on the listing below. Note that Rocket
and Smile
classes are interconnected via ListBox.Item
interface.
public class ListBox
{
public interface Item
{
string Name {get;}
}
public void Add(Item item)
{
string name = item.Name;
Console.Out.WriteLine(name);
}
}
public class Rocket : ListBox.Item
{
private string name;
public Rocket(string name)
{
this.name = name;
}
public string Name
{
get { return name; }
}
}
public class Smile : ListBox.Item
{
private string name;
public Smile(string name)
{
this.name = name;
}
public string Name
{
get { return name; }
}
}
public static void Test()
{
ListBox box = new ListBox();
Rocket rocket = new Rocket("progress");
Smile smile = new Smile("gigling");
box.Add(rocket);
box.Add(smile);
}
The ad hoc method is to check the presence of the Name
property in the object just before getting it. The classes remain unconnected. But this method is unsafe and brings in the maintenance nightmare. You are responsible to manually check the presence of the Name
property every time a new class is added or the present class is changed. Otherwise, unexpected errors will be produced at runtime. It is funny to receive "looks like you forgot to implement the Name
property in the Rocket
class" bug report from the customer.
The compile-time method is to define ListBox.Add
method as a template method. Thus, the presence of the Name
property will be checked at compile-time for each object that will be added. If the Name
is absent, then a compilation error will be generated. The main drawback of this method is that part of your code becomes static. For example, it is impossible to define IListBox
interface with the Add()
template method, because a template method cannot be virtual.
Adaptable polymorphism
There is another method that often even isn't listed in the literature. The idea is to provide an adapter from each class to the Item
interface. Thus, the classes remain unconnected. It is safe, because when the new class is added, it is impossible to forget the creation of the new adapter. You just get a compilation error, which is much better than the runtime one.
Adaptable polymorphism is efficient, because only one additional method call is needed. It is faster than ad hoc polymorphism but a little bit slower than the compile-time one.
The adaptable code for the previous example is shown below:
public class ListBox
{
public interface Item
{
string Name {get;}
}
public void Add(Item item)
{
string name = item.Name;
Console.Out.WriteLine(name);
}
}
public class Rocket
{
private string name;
public Rocket(string name)
{
this.name = name;
}
public string Name
{
get { return name; }
}
}
public class Smile
{
private string name;
public Smile(string name)
{
this.name = name;
}
public string Name
{
get { return name; }
}
}
public class RocketItemAdapter : ListBox.Item
{
private Rocket handler;
public RocketItemAdapter(Rocket rocket)
{
this.handler = rocket;
}
public string Name
{
get
{
return handler.Name;
}
}
}
public class SmileItemAdapter : ListBox.Item
{
private Smile handler;
public SmileItemAdapter(Smile handler)
{
this.handler = handler;
}
public string Name
{
get
{
return handler.Name;
}
}
}
public static void Test()
{
ListBox box = new ListBox();
Rocket rocket = new Rocket("progress");
Smile smile = new Smile("gigling");
box.Add(new RocketItemAdapter(rocket));
box.Add(new SmileItemAdapter(smile));
}
The main restriction of this method is that programmers are lazy. It is tedious to write a lot of adapters. Suppose that Item
contains twelve methods and you have one hundred classes that can be listed in the ListBox
.
Adapter class template can be created in C++, but nevertheless, the Item
idiom will be at least doubled (in the Item
itself and in the adapter template class). Of course, it is less painful than writing adapters for each class like in C#.
The adapt keyword
The question is why programming languages don't provide ad hoc adaptation facility. I'm really wondering on this fact. The solution is very easy. Look at the uncompilable code below:
public class ListBox
{
public interface Item
{
string Name {get;}
}
public void Add(Item item)
{
string name = item.Name;
Console.Out.WriteLine(name);
}
}
public class Rocket
{
private string name;
public Rocket(string name)
{
this.name = name;
}
public string Name
{
get { return name; }
}
}
public class Smile
{
private string name;
public Smile(string name)
{
this.name = name;
}
public string Name
{
get { return name; }
}
}
public static void Test()
{
ListBox box = new ListBox();
Rocket rocket = new Rocket("progress");
Smile smile = new Smile("gigling");
box.Add(adapt<ListBox.Item>(rocket));
box.Add(adapt<ListBox.Item>(smile));
}
All what I want from the compiler is automatic implicit compile-time creation of RocketItemAdapter
and SmileItemAdapter
classes. It is easy to do for the compiler because all the information is present here. After all, this feature is much simpler than C++ style templates.
That's all. Just ask your compiler vendor to improve your beloved language. And if the vendor is a good man, then you can play with the adapt
keyword as early as next week.
The emulation of adapt
keyword in C# v.1 language is discussed below just to make sure. I believe that there are no ugly and sluggish compiler vendors.
The AHA library
The main idea of the AHA library is to emit the adapter at runtime. It can be easily done using System.Reflection.Emit
namespace tools. The main difficulty is to make it in a type safe manner. It's easy to fall into the unsafe ad hoc polymorphism. I want to get a compile-time error when it's impossible to emit the adapter at runtime.
It is a funny goal in itself. Moreover, it seems to be unachievable. That's why the AHA library produces errors at initialization time. It's worse than compile-time, but much better than runtime. You are responsible to run your program once before releasing it. If there is an invalid adaptation request anywhere in your program, then your program crashes immediately after startup.
To make a long story short, look at the code below:
public class ListBox
{
public interface Item
{
string Name {get;}
}
public void Add(Item item)
{
string name = item.Name;
Console.Out.WriteLine(name);
}
}
public class Rocket
{
private string name;
public Rocket(string name)
{
this.name = name;
}
public string Name
{
get { return name; }
}
}
public class Smile
{
private string name;
public Smile(string name)
{
this.name = name;
}
public string Name
{
get { return name; }
}
}
[Adaptation(typeof(ListBox.Item), typeof(Rocket))]
public class RocketItemRequest : AdaptationRequest
{
private Rocket rocket;
public RocketItemRequest(Rocket rocket)
{
this.rocket = rocket;
}
public override object Handler
{
get
{
return rocket;
}
}
}
[Adaptation(typeof(ListBox.Item), typeof(Smile))]
public class SmileItemRequest : AdaptationRequest
{
private Smile smile;
public SmileItemRequest(Smile smile)
{
this.smile = smile;
}
public override object Handler
{
get
{
return smile;
}
}
}
private static void Test()
{
ListBox box = new ListBox();
Rocket rocket = new Rocket("progress");
Smile smile = new Smile("gigling");
box.Add((ListBox.Item)AdapterKit.Adapt(new RocketItemRequest(rocket)));
box.Add((ListBox.Item)AdapterKit.Adapt(new SmileItemRequest(smile)));
}
[STAThread]
static void Main(string[] args)
{
AdapterKit.Anchor();
Test();
}
There are the following interesting points in the program above:
- The
RocketItemRequest
class provides full specification for making the adapter from Rocket
to Item
. First of all, the interface and the handler are listed in the Adaptation
attribute. Secondly, the adaptable objects for this request are restricted to the Rocket
class and all its descendants, by the constructor signature.
- Adapters are created using
AdapterKit.Adapt()
static method.
- The consistency checking is requested by calling
AdapterKit.Anchor()
static method.
The irony is that RocketItemRequest
is as long as the manually written RocketItemAdapter
. But the RocketItemRequest
has a constant size and doesn't depend on the size of the Item
interface. The size of the request doesn't grow when new methods are added to Item
. Moreover, all requests have a similar structure and can be generated on the fly by your IDE.
How consistency checking works
During consistency checking, all the adaptation requests are found using the Assembly.GetTypes()
method. The Adaptation
attribute is analyzed for each request and the library tries to create the appropriate adapter. It the adaptation fails, then the program fails by exception immediately. Created adapters are cached for future use (maybe, it's a premature optimization).
Thus, there is no companion consistency checking tools, no post build steps or other configuration troubles. Your program remains consistent by default.
To enable consistency checking, you are responsible to call AdapterKit.Anchor()
method manually. But it's hard to forget because AHA library doesn't work without anchoring. The assumption is made that when the first adaptation request is written, the programmer will look at how it works, for curiosity reasons at least.
The AHA performance
The first question is how fast it is. And the answer is "it's as fast as possible". It's because plain IL code is generated for each method in the interface. It's as fast as writing adapters for each class manually.
The AhaSpeedTest
example provides the following results:
Handler is directly inherited from the interface
Elapsed time: 00:00:02.5436576
Handler is wrapped by manually written adapter
Elapsed time: 00:00:02.9141904
Handler is wrapped by automatically emitted adapter
Elapsed time: 00:00:02.7139024
As you can see, the automatically wrapped version is even faster than the manual one. It's because OpCodes.Call
is used instead of OpCodes.Callvirt
. The C# compiler doesn't know the particular type of the handler and is forced to call methods virtually. The AHA library knows the particular type of the handler when the adapter is emitted and can avoid virtual calls. But this choice brings us to a bigger set of adapters that are generated.
How it differs from generics
Maybe, I don't understand generics. Maybe, they're described incorrectly. Maybe, they're implemented incorrectly in my beta of C# v.2 compiler. But it looks like they're designed incorrectly. It's because they don't provide a solution for the discussed problem (as the C++ templates do).
I cannot write code on C# 2.0 better than the following:
public class ListBox
{
public interface Item
{
string Name {get;}
}
public void Add<T>(T item) where T : Item
{
string name = item.Name;
Console.Out.WriteLine(name);
}
}
public class Rocket : ListBox.Item
{
private string name;
public Rocket(string name)
{
this.name = name;
}
public string Name
{
get { return name; }
}
}
public class Smile : ListBox.Item
{
private string name;
public Smile(string name)
{
this.name = name;
}
public string Name
{
get { return name; }
}
}
public static void Test()
{
ListBox box = new ListBox();
Rocket rocket = new Rocket("progress");
Smile smile = new Smile("gigling");
box.Add(rocket);
box.Add(smile);
}
Note that Rocket
and Smile
must be inherited from the Item
interface. And the constraint where T: Item
must be provided. Thus, the generic code isn't better than the first example in this article. The Rocket
and Smile
classes are still interconnected and must be designed and implemented with the notion of ListBox.Item
interface.
Main differences from ad hoc polymorphism
There are other frameworks that can build adapters on the fly.
It's easy to introduce this feature with the NMock library. There is an AutoCaster class that provides similar facility.
The main difference of the proposed approach is the improved safety. For example, let us consider a modification of the AutoCaster accompanying example (see full original code here):
Note: this code works only with the second version of C#.
public class WrongTest
{
public void DoIt()
{
Console.WriteLine("yeah");
}
}
class TestMain
{
public static void Main()
{
try
{
Test test = new Test();
ITest itest = null;
if (DateTime.Now.DayOfWeek == DayOfWeek.Sunday)
itest = Latent<ITest>.Cast(new WrongTest());
else
itest = Latent<ITest>.Cast(test);
itest.DoIt();
itest.Print("Hello", "World");
itest.Print(1, 2);
Console.WriteLine(itest.Sum(1, 2));
}
catch (Exception e)
{
Console.WriteLine(e);
Console.WriteLine(e.StackTrace);
}
}
}
The above code doesn't work on Sundays. But it works properly on each other day of the week. So it's very hard to find this problem.
Similar example for AHA is more verbose but more safe. Just because the Sunday problem is visible on each other day of week.
public interface ITest
{
void DoIt();
}
public class Test
{
public void DoIt()
{
Console.Out.WriteLine("It's done!");
}
}
public class WrongTest
{
public void WrongDoing()
{
Console.Out.WriteLine("It's cannot be done!");
}
}
[Adaptation(typeof(ITest), typeof(Test))]
public class TestRequest : AdaptationRequest
{
}
[Adaptation(typeof(ITest), typeof(WrongTest))]
public class WrongTestRequest : AdaptationRequest
{
}
[STAThread]
static void Main(string[] args)
{
AdapterKit.Anchor();
ITest itest = null;
if (DateTime.Now.DayOfWeek == DayOfWeek.Sunday)
itest =
(ITest)AdapterKit.Adapt(new WrongTestRequest(new WrongTest()));
else
itest = (ITest)AdapterKit.Adapt(new TestRequest(new Test()));
itest.DoIt();
}
Despite the verbosity, this feature looks very important. The implementation of this feature is easy and transparent.
A word about managed C++
This technique is also applicable to Managed C++. Moreover, for C++, there is a better solution. No explicit adaptation requests and anchoring are needed. The C++ templates can be leveraged to make this technique type safe. A short example of managed C++ code is shown below:
IFoo* foo = AdapterKit<IFoo, Handler>::Adapt(new Handler());
Consistency checking can be done in the static field constructor of AdapterKit<IFoo, Handler>
.
But the managed C++ version of the AHA isn't released yet. C++/CLI is still unavailable and managed C++ is still unusable.
How to get the AHA library
The AHA library is free and is available for downloading from www.vistal.sf.net. Now the library is a part of the ViStaL project.
Future directions
Following things are planned for the AHA:
- The choice of what is preferable to the user - speed or size. These goals can be achieved by using or not
OpCodes.Callvirt
in the adapters.
- Caching of the generated adapters on the hard disk. The adaptation requests set usually aren't changed between execution sessions and can be cached.
- Post-build consistency checking tool that can be useful when it's hard to run the program because of its deployment process.
- Good name resolving algorithm.
References
Some ideas and a lot of code are borrowed from the NMock library.