Saturday, April 25, 2009

Generic Methods and Type Inferencing

In other words - as I understand it - Tiburón/Delphi 2009 will not (initially at least) support type inferencing. To my mind this dramatically reduces the attractiveness of Generic Methods.

What Is Type Inferencing?


As the term suggests, (in this context at least) it is the ability of the compiler to infer the type of some symbol (variable or parameter etc) from the context or code around it.


In C# for example, it means that variables can be declared:


  var a = 10;

The compiler will infer from this that the type of variable a is int (you knew that, right?) without this having to be explicitly declared.


Pop Quiz: What do we think would/should the type of a be if Delphi were to support this?


Hint: What type would a be if it were a constant?


In general I find this to have dubious value, what, for example, can you deduce about the correct usage of a from this:


  var a = someRef.DisplayValue;

To know the type of a you know need to know the type of someRef in order to in turn know the return type of DisplayValue. If browsing a project with all references intact and Code Insight, or equivalent, to deliver this information to you, then all good. But if not, you are going to have a sticky time of it, and either way you still have to trust that the person that wrote the code - which perhaps wasn’t you - also knew those things and didn’t make their own inferencing error.


But what does type inferencing have to do with Generic Methods?


To understand that let’s look at why Generic Methods even come into it.


It all started this evening when I found myself needing to add another overload to my set of Exchange() methods, two of which I show here to give you the idea:


  procedure Exchange(var A, B: Integer); overload;
procedure Exchange(var A, B: TObject); overload;

(The Exchange procedure exchanges the values of the two passed parameters)


My problem was that because the parameters to these procedures are, by necessity, var parameters, the compiler enforces strict type checking, so the second of the above declarations cannot be used with variables of a TObject derived type, but only variables explicitly and specifically of TObject type itself:


  var
obj1, obj2: TObject;
form1, form2: TForm;
begin
:
Exchange(obj1, obj2); // OK - references will be exchanged
Exchange(form1, form2); // ERROR: No compatible overload
end;

The second call to Exchange() will not compile because the TForm type parameters are not compatible (from the compiler’s perspective) with the TObject overloaded version of the routine. I have to add another overload with explicitly TForm type parameters.


Not for the first time recently I found myself thinking “If only I had Delphi 2009 - Generic Methods would make this so much easier!”. Then I remembered reading Barry Kelly’s note about lack of type inferencing in Delphi 2009, and an alarm bell started ringing.


The Generic Solution


A generic implementation of an Exchange() procedure should be simple enough, and would go a little something like this:


  procedure Exchange<T>(var A, B: T);
var
i: T;
begin
i := A;
A := B;
B := i;
end;

Which is nice and neat and cuts down on all those overloads. Unfortunately however, using this procedure is actually now more cumbersome, not less:


  var
obj1, obj2: TObject;
form1, form2: TForm;
begin
:
Exchange<TObject>(obj1, obj2);
Exchange<TForm>(form1, form2);
end;

Ouch.


Frankly for the time it takes to create a new overload of a trivial routine like this, I would rather take those few seconds and reap the rewards later since - in common with most such routines - the code for the procedure will be written only once but code to call it will be written many, many times.


Voices Off: “But lots of overloads will pollute your namespace!”


You know what? That bothers me a lot less than creating unnecessary work for myself. I’m not the sort of developer that is unable to produce a line of code without invoking Code Completion. I may be unusual in this day and age, but I still write code faster than I can pick it from drop-down lists.


And this in Win32 Delphi at least is surely the biggest impact? As far as the code goes, any unused overloads will be pruned out by the linker. In .NET - aiui - a cluttered namespace becomes a public nuisance, so the imperatives are somewhat different perhaps.


How Would Type Inferencing Help?


Well, assuming that any future Delphi type inferencing system could determine the appropriate type from the parameters (as it can in C#) then we could invoke our generic Exchange<T>() method by simply writing:


  var
obj1, obj2: TObject;
form1, form2: TForm;
begin
:
Exchange(obj1, obj2);
Exchange(form1, form2);
end;

i.e. just as we can do with overloads.


But until this is possible at this stage I was feeling that I would continue with overloads until - at least - type inferencing were available. They are just as type-safe and ironically produce “consumer” code that should be entirely compatible with a future generic methods implementation that is bolstered by type inferencing.


Using generic methods without type inferencing (in these sorts of cases at least) will simply create unnecessarily verbose and cumbersome “consumer” code.


But not being one to give in, I considered some alternative approaches.


A Truly generic Approach?


Alternatively, Delphi already provides a means to implement a truly generic (lower case “g’) Exchange() method - untyped parameters:


  procedure Exchange(var A, B);
var
i: ?
begin
i := A;
A := B;
B := i;
end;

You will immediately notice of course that this implementation is neither valid nor complete. The type of i (for “intermediate”, if you were wondering) is not known and indeed not knowable. And furthermore, the compiler simply won’t accept that A := B assignment since it doesn’t know the types of A and B it cannot know what instructions are needed. For the same reason, the XOR trick won’t work either. Those untyped parameters seem to have lead us to a dead end.


Not quite. (If you are squeamish and/or don’t like smelly code you might want to avert your gaze about now, or at least pinch your nose):


  procedure Exchange(var A, B);
var
aa: Integer absolute A;
bb: Integer absolute B;
i: Integer;
begin
i := aa;
aa := bb;
bb := i;
end;

The absolute keyword is not something to be used lightly - it tells the compiler that the variables aa and bb exist at the same location as the parameters A and B - in this case the compiler even takes care of the fact that the parameters are passed by reference.


Since the variables are typed and the parameters are untyped, how can this be safe?


Well, frankly it isn’t.


In practice if the type of A and B is not the same size as an Integer (32-bits) then things are not going to go at all according to plan.


Equally of course though, if you only ever call this implementation routine with 32-bit sized parameters (which includes strings, object references etc) there won’t be a problem. “IF”.


I should point out at this stage that I explored this approach as a curiosity. I am certainly NOT recommending it! For one thing it is not “truly generic” at all - it only appears to be but in fact has fairly strict conditions for correct use, and does not benefit from any assistance from the compiler to ensure that you do in fact use it correctly!


But we have one more trick up our sleeve - there is (at least) one more way to skin this particular cat that is safer than an untyped parameter approach and only a little more cumbersome to use than a non-type-inferenced Generic approach.


X-Rated Code - Being Explicit


A var parameter is syntactic sugar for passing by referencing and allowing modification of the de-referenced value. We can of course achieve the same thing by taking care of the de-referencing aspects ourselves and explicitly passing references to values, rather than the values themselves:


  type
PObject = ^TObject;

:

procedure Exchange(const A, B: PObject);
var
i: TObject
begin
i := A^;
A^ := B^;
B^ := i;
end;

And to call this:


  var
obj1, obj2: TObject;
form1, form2: TForm;
begin
:
Exchange(@obj1, @obj2);
Exchange(@form1, @form2);
end;

All of which sits happily alongside any other overloaded versions of Exchange(), but which unfortunately requires consumer code that will not be compatible with a future Generic Method implementation of the routine.


BUT, even this won’t work if we are compiling with Typed @ Operator option enabled.


So I’m left a little stumped.


There are many ways to go about this. If it weren’t for the fact that Generic Methods are nearly upon us I would favour the explicit de-referencing approach. But the desire to create code today that will be compatible with impending new language features is quite compelling. Then again, until we also get type inferencing, a Generic Methods based approach isn’t going to be compatible with anything we can write today anyway.


Despite myself, the untyped parameter approach could yet prove too tempting to resist (particularly if I find myself needing yet another class-specific version of Exchange()) since all the types that I’ve ever found myself wanting to Exchange() meet the 32-bit criteria.


It really hinges on when we might see type inferencing in a Delphi compiler. If that is something we are likely only to see in Commodore then worrying about compatability of a few calls to Exchange() could prove somewhat misplaced given the far wider issues likely to arise from the move to a 64-bit compiler.


A Compromise?


In the meantime I wonder whether it would not be possible to have a compiler option to disable such strict type checking on var parameters, ideally on a method-by-method basis.


After all, we still have such an option for short string var parameters (although having never had cause to use it I don’t know if it works in quite the way I have in mind - i.e. on declarations of rather than calls to, methods).


In the case of class-type var parameters the more relaxed type checking could allow parameters of any type correctly derived from the formal type:


  interface
{$VARSTRICTCLASSTYPE OFF}
procedure Exchange(var A, B: TObject);
{$VARSTRICTCLASSTYPE ON}

implementation

procedure Exchange(var A, B: TObject);
:
end;

Then again, I can’t think of any other concrete examples where such a capability would have any practical use. But if this were possible to implement more quickly and safely than a comprehensive type inferencing system, I think I’d take it to keep me going.

No comments: