A few weeks ago (I've collected many links in my "for blog" bookmarks recently) Danny Thorpe blogged about Dynamic Method Invocation, claiming Delphi did years ago what C# is aiming at now (the history of Delphi leading on the tech side is repeating). His post was a response to the post of yet-another ex-Borlander, Charlie Calvert, who posted about Dynamic Lookups in C# in a Future Focus column of his blog. I find the proposed C# syntax confusing, and don't think that making out the section of code for dynamic invocation at the caller side makes a lot of sense, but I had only a cursory view.
Let me start (today) by showing you how the call looks like:
var
v: variant;
begin
v := VarDSLDateTimeCreate (now);
v.NextMonday;
v.am7;
This sets the date and time represented by the variant to 7 am next Monday. I had a much better invocation mechanism by embedding the variant within an actual object, something I'll work on for a future post. Before that, I'll show you the custom variant architecture. All I want to show you for today, is that NextMonday and 7am are not hard-coded functions. They don't exist. The object receives a call and parses and interprets the function name to determine what to do! Here is a snippet, I'll expand soon with the details:
function TDslDateTimeVariantType.DoProcedure(const V: TVarData;
const Name: string; const Arguments: TVarDataArray): Boolean;
var
tmp: string;
value, month, day: Integer;
tmpDate: TDateTime;
begin
if Pos ('AM', Name) > 0 then
begin
// parse and process...
tmp := StringReplace (Name, 'AM', '', []);
value := StrToIntDef (tmp, 0);
// yes this is ugly!!!
TDslDateTimeVarData(V).VDateTime.DateTime :=
RecodeTime (TDslDateTimeVarData(V).VDateTime.DateTime, value, 0, 0, 0);
end;
...
if Pos ('NEXT', Name) > 0 then
begin
// parse and process...
tmp := StringReplace (Name, 'NEXT', '', []);
if tmp = 'MONDAY' then
begin
// compute next monday date...
...
Stay tuned for the rest of the code and an improved version of it.
Dynamic Method Invocation in Delphi (Part 2)
My post yesterday on dynamic methods invocation in Delphi has raised a few doubts about the rationale behind sch an "unnatural" way of writing Delphi code. I should have posted the rationale behind dynamic method invocation before starting showing code... but at least I'm doing it before delving into the actual tech details, a post I originally wanted to do today, but I'm delaying.
So do you want to invoke a method that doesn't exists at compile-time? There isn't a single answer, but I have used similar coding in real projects and have seen trends (mostly in other languages) going in this direction. So here is a short list of reasons:
- It is trendy. I know, this is not a technical reason. But with people who keep complaining Delphi is always behind every other language, I think it is worth pointing out Delphi has features that other popular languages (Java, C#) are thinking of adding now. And why are they adding this features? Mostly because they are trendy, and they have become trendy because Ruby on Rails is heavily based on them. Again, does it make any sense for Delphi to clone Ruby features? Certainly not, but claiming "hey we can do it as well" is certainly legitimate.
- Dynamic invocation is a common features of dynamic languages, from Ruby to JavaScript, which are not compiled in a strict sense. As Delphi developers you probably share with me some aversion for slow, late-bound, non compile-checked language features. I won't certainly use them as a standard coding practice. However there are situations in which we end up doing the same, in an even worse way. have you even accessed a property by name with RTTI? Have you ever passed a database field name (the string, I mean) as parameter? Every time you use a string to "parameterize" the meaning of a function call you are delaying the resolution of a symbol at runtime. With dynamic invocation you are doing exactly the same, only you are using the language base syntax rather than a convoluted string based one. How comes is setting a dynamic property with a variant is much worse than calling SetPropValue? The former can be more readable.
- Although I'm a string supporter of strongly typing and related features, I appreciate a lot when flexibility is build into a program in a way I don't have to recompile it when I could only reconfigure it. From DFM files to configuration files, to applications that change depending on database metadata, there are many cases in which you want flexibility into your programs.
- I've used this technique while toying with Domain Specific Languages. An internal DSL is like a sub-language hosted by your language and written according to its syntax, but meant almost for non programmers, easy to read and understand, not too bound to the language syntax. This is the situation in which calling date.am7 could be better that data.at (7, 'am'), the former is easier to write and read, especially for a non-programmer. You might not like the entire DSL movement, but I find it quite interesting.
- Coding against partially undefined (at compile time) or flexible data structures is quite neat. Won't it be nice to say table1.usename when the latter is a field of the table rather than writing table1.fieldbyname('username')? Sure, Delphi can generate the field definitions for you with the fields editor, but when you hit a table with a slightly different structure you are stuck. I ended using a similar solution to the one outlined here for XML DOM navigation. In my code (well, I wrote it with others) you could write data.user.name to get the name node under the user node of the document. Of course, the document needed to have those specific nodes or you'd have a runtime error. But the XML document could have different formats provided those nodes where present, something you don't get if you import the data structure at compile time and let Delphi define interfaces for you.
I might not have convinced you, I know, but here are some reasons I find interesting for using dynamic method invocation when it is needed. I will never give up strong type-checking as a general compiler rule, but I appreciate exceptions. The trouble, though, is that writing this code in Delphi is much more difficult than I'd like... but that something for another post (which might take a few day, due to Easter holidays. For now, if you have other reasons for using (or not using) dynamic methods invocation feel free to comment here (or in yuor blog, posting a link here, as you prefer).
To summarize, Delphi is more powerful and flexible than most of its user think. Let's use that power when needed...
Dynamic Method Invocation in Delphi (Part 3)
After two introductory posts, here is the actual source code of the custom variant used to implement dynamic method invocation. It is only a rough working prototype. Time permitting, I'd integrate it into the actual class hosting the data (something I did in the past for a different demo). And will try to make the code easier to reuse. Anyway, here is goes. This rather complex code allows you to call method that don't actually exist, as you'll pass the method name as a string parameter to a single processing function. The same could be done for functions and properties. Enjoy.
unit
DynamicDslVariant;interface
uses
TypInfo, Variants, Classes;function
VarDSLDateTimeCreate(const
AValue: TDateTime): Variant;overload
;implementation
uses
VarUtils, SysUtils, DateUtils, WIndows, Dialogs;type
TDslDateTimeVariantType =class
(TPublishableVariantType)protected
function
GetInstance(const
V: TVarData): TObject;override
;public
procedure
Clear(var
V: TVarData);override
;procedure
CastTo(var
Dest: TVarData;const
Source: TVarData;const
AVarType: TVarType);override
;function
DoFunction(var
Dest: TVarData;const
V: TVarData;const
Name:string
;const
Arguments: TVarDataArray): Boolean;override
;function
DoProcedure(const
V: TVarData;const
Name:string
;const
Arguments: TVarDataArray): Boolean;override
;end
;var
DslDateTimeVariantType: TDslDateTimeVariantType =nil
;type
TDslDateTimeData =class
(TPersistent)private
FDateTime: TDateTime;public
constructor
Create(const
AValue: TDateTime);overload
;function
AsString:string
;property
DateTime: TDateTimeread
FDateTimewrite
FDateTime;end
;{ Helper record for variant data }
TDslDateTimeVarData =packed
record
VType: TVarType; Reserved1, Reserved2, Reserved3: Word; VDateTime: TDslDateTimeData; Reserved4: DWord;end
;{ TDslDateTimeVariantType }
procedure
TDslDateTimeVariantType.CastTo(var
Dest: TVarData;const
Source: TVarData;const
AVarType: TVarType);var
LTemp: TVarData;begin
if
Source.VType = VarTypethen
case
AVarTypeof
varString: VarDataFromStr(Dest, TDslDateTimeVarData (Source).VDateTime.AsString);else
VarDataInit(LTemp);try
LTemp.VType := varDate; LTemp.VDate := TDslDateTimeVarData (Source).VDateTime.DateTime; VarDataCastTo(Dest, LTemp, AVarType);finally
VarDataClear(LTemp);end
;end
else
inherited
;end
;procedure
TDslDateTimeVariantType.Clear(var
V: TVarData);begin
V.VType := varEmpty; FreeAndNil(TDslDateTimeVarData(V).VDateTime);end
;function
TDslDateTimeVariantType.DoFunction(var
Dest: TVarData;const
V: TVarData;const
Name:string
;const
Arguments: TVarDataArray): Boolean;begin
ShowMessage ('fn calling '
+ name);// Dest :=
Result := True;end
;function
TDslDateTimeVariantType.DoProcedure(const
V: TVarData;const
Name:string
;const
Arguments: TVarDataArray): Boolean;var
tmp:string
; value, month, day: Integer; tmpDate: TDateTime;begin
// ShowMessage ('proc calling ' + name);
if
Pos ('AM'
, Name) > 0then
begin
// parse and process...
tmp := StringReplace (Name,'AM'
,''
, []); value := StrToIntDef (tmp, 0); TDslDateTimeVarData(V).VDateTime.DateTime := RecodeTime (TDslDateTimeVarData(V).VDateTime.DateTime, value, 0, 0, 0);end
;if
Pos ('PM'
, Name) > 0then
begin
// parse and process...
tmp := StringReplace (Name,'PM'
,''
, []); value := StrToIntDef (tmp, 0); TDslDateTimeVarData(V).VDateTime.DateTime := RecodeTime (TDslDateTimeVarData(V).VDateTime.DateTime, value + 12, 0, 0, 0);end
;if
Pos ('NEXT'
, Name) > 0then
begin
// parse and process...
tmp := StringReplace (Name,'NEXT'
,''
, []);if
tmp ='MONDAY'
then
begin
tmpDate := Now; value := DayOfWeek(tmpDate); tmpDate := tmpDate + 7 - value + 2;// monday as 2nd day of the week
TDslDateTimeVarData(V).VDateTime.DateTime := DateOf (tmpDate) + TimeOf (TDslDateTimeVarData(V).VDateTime.DateTime);end
;end
;if
Pos ('DEC'
, Name) > 0then
begin
month := 12;// parse and process...
tmp := StringReplace (Name,'DEC'
,''
, []); day := StrToIntDef (tmp, 0); TDslDateTimeVarData(V).VDateTime.DateTime := RecodeDate (TDslDateTimeVarData(V).VDateTime.DateTime, YearOf (now), month, day);end
; Result := True;end
;function
TDslDateTimeVariantType.GetInstance(const
V: TVarData): TObject;begin
Result := TDslDateTimeVarData(V).VDateTime;end
;{ TDslDateTimeData }
function
TDslDateTimeData.AsString:string
;begin
Result := DateTimeToStr (FDateTime);end
;constructor
TDslDateTimeData.Create(const
AValue: TDateTime);begin
FDateTime := AValue;end
;// variant construction
function
VarDSLDateTimeCreate(const
AValue: TDateTime): Variant;begin
VarClear(Result); TDslDateTimeVarData(Result).VType := DslDateTimeVariantType.VarType; TDslDateTimeVarData(Result).VDateTime := TDslDateTimeData.Create(AValue);end
;initialization
DslDateTimeVariantType := TDslDateTimeVariantType.Create;finalization
FreeAndNil(DslDateTimeVariantType);end
.