Thursday, September 25, 2008

Another "MacGyver" moment

Or, "More fun with Generics and Anonymous methods".

I'll just leave it up to you whether or not these utility functions are useful, but here they are:

type
Obj = class
class procedure Lock(O: TObject; Proc: TProc); static;
class procedure Using<T: class>(O: T; Proc: TProc<T>); static;
end;

class procedure Obj.Lock(O: TObject; Proc: TProc);
begin
TMonitor.Enter(O);
try
Proc();
finally
TMonitor.Exit(O);
end;
end;

class procedure Obj.Using<T>(O: T; Proc: TProc<T>);
begin
try
Proc(O);
finally
O.Free;
end;
end;

While very contrived, here's how you could use Obj.Using():

procedure TForm1.Button1Click(Sender: TObject);
begin
Obj.Using<TStringList>(TStringList.Create, procedure (List: TStringList)
begin
List.Add('One');
List.Add('Two');
List.Add('Three');
List.Add('Four');
ListBox1.Items := List;
end);
end;

And here's how you could use Obj.Lock():

procedure TMyObject.Process;
begin
Obj.Lock(Self, procedure
begin
//code executing within critical section
end);
end; 

24 comments:

  1. I like the Using example. That's a nice way to deal with needing a short lived object to do a minor bit of work. It's also a neat way to implement a feature from another language that's not natively supported.

    Pretty cool.

    ReplyDelete
  2. Interesting. Almost, but not quite, entirely unlike smart pointers ;-)

    Something I haven't got my head round yet is this: Do the new Delphi Generics allow you to implement anything more closely resembling boost's scoped ptrs and other RAII techniques? Or is "Finally" still the single most common keyword apart from begin/end?

    ReplyDelete
  3. Those are pretty creative. Generics and Anonymous really open the language up to a lot of creative usages like that.

    ReplyDelete
  4. This is pretty cool and I will probably start using that once my Delphi 2009 arrives (here in Germany the English versions aren't available yet and my trial has expired).

    Now if we had a Type Inference and a little more concise Anonymous Functions syntax it would look even sweeter:

    Obj.Using(TStringList.Create, List =>
    List.AddString('2')
    );

    Maybe next version...one can always hope ;)

    ReplyDelete
  5. Is it possible to use Exit(Something) inside of the anonymous function to exit an enclosing function? Is "Result" available in there?

    ReplyDelete
  6. > here in Germany the English versions aren’t available yet

    There is only one version with all supported languages. If I want I could install Delphi 2009 in Japanese (what I won't do because I wouldn't understand a single word :-) )

    ReplyDelete
  7. For the using case, I have an expirimental implementation that doesn't need anonymous methods:

    with Using.This(TStringList.Create) do
    begin
    Add('Hello');
    end;

    It uses Interfaces in the implemenation to keep reference

    A second form of which is was thinking (but did not implement yet)

    var: Str: TStringList;
    ...
    with Using.This(Str, TStringList.Create) do
    begin
    Str.Add('Hello');
    end;

    ReplyDelete
  8. Man the &lt and &gt where removed from both methods...

    both methods above are generic and so it was called like:

    Using.This&ltTStringList&gt(...

    Tjipke

    ReplyDelete
  9. Daniel,

    "Is it possible to use Exit(Something) inside of the anonymous function to exit an enclosing function? Is "Result" available in there?"

    No. The Anonymous function *is* a proper function and so it has it's own scope. "Exit" would only exit the anonymous function itself. The "Result" variable is not captured and is not available.

    Allen.

    ReplyDelete
  10. Very nice post...

    ReplyDelete
  11. Tjipke, your code does not achieve the goal. "Using" statements provide two things for an object: limited scope and limited lifetime.

    Your first example does not provide any scope at all; there is no variable to use to refer to the new object. The second example uses the scope of a normal local variable.

    Neither of your examples gives a limited lifetime to the new object. You say you use interfaces, but they only have their reference counts decremented when the enclosing routine exits. They are not controlled by simple statement blocks. Leaving the "with" block will not cause any interfaces to be released.

    ReplyDelete
  12. I would prefer Static Class Instantiation. C++ can do, Delphi not. :-(

    ReplyDelete
  13. Rob Kennedy, thanks for your observation, but it is not correct. (But I guess that is my fault for not giving the implementing code, I don't have that currently here)

    Basically the limited scope is within the "with" block. The result of Using.This is an interface that contains the thing that is passed in (the stringlist in this case), so the scope is implicit.
    There is a bug in the first example: it should read UsingObject.Add('...'). (Guess that happens when you try to retrieve the code from memory). UsingObject is a property of that interface containing the stringlist.
    I also realized that always needing to use UsingObject to access the object you are using is no not be very 'readable' (and doesn't work for nested usings) and that's why it is still experimental. The second form is my current suggestion (to myself) for improving it.

    Then about the lifetime: it is maintained by the interface The result of Using.This is a reference to an interfaced object. When the with goes out of scope, the interfaced object is freed, and it frees the contained object.

    I hope it is clear. Sorry but the real code is currently on a VM that I can't reach now, I was hoping to release it to the public sometime...

    Tjipke

    ReplyDelete
  14. Please explain. I must admit I haven't seen the light yet.

    Why clutter up the Pascal syntax with this? The example shows the temporary use of a TStingList object. I can do that already:

    var
    List: TStringList;
    begin
    List := TStringList.Create;
    try
    List.Add('One');
    List.Add('Two');
    List.Add('Three');

    // This assignment should strictly spoken not do a
    // contents assignment but replace the pointer of the
    // List-object. The result is suspect code
    ListBox1.Items := List;

    finally
    List.Free;
    end;
    end;

    Yes. There's a few more lines but that's that. So where is the big invention?

    ReplyDelete
  15. Would be nice if future versions of Delphi supported a shortcut syntax for anonymous methods.

    So instead of:
    ---
    Obj.Lock(Self, procedure
    begin
    //code executing within critical section
    end);
    ---

    One could write:
    ---
    Obj.Lock(Self) do
    begin
    //code executing within critical section
    end;
    ---
    So if the last parameter is a TProc (so basically any methods that follow that signature), then the "do" syntax can be used for slightly cleaner looking code.

    And:
    ---
    Obj.Using(List: TStringList) do
    begin
    List.Add(’One’);
    List.Add(’Two’);
    List.Add(’Three’);
    List.Add(’Four’);
    ListBox1.Items := List;
    end;
    ---

    This syntax will be optional, but idea is it can be used anywhere when the last parameter is a TProc, TFunc, etc. (i.e. "reference to ..." type) then the compiler will allow the "do" syntax.

    Even your Synchronize method will look cleaner:
    From this:
    ---
    Synchronize(procedure
    begin
    with FBox do
    begin
    Canvas.Pen.Color := clBtnFace;
    PaintLine(Canvas, I, A);
    PaintLine(Canvas, J, B);
    Canvas.Pen.Color := clRed;
    PaintLine(Canvas, I, B);
    PaintLine(Canvas, J, A);
    end;
    end);

    ---

    to this:
    ---
    Synchronize do
    begin
    with FBox do
    begin
    Canvas.Pen.Color := clBtnFace;
    PaintLine(Canvas, I, A);
    PaintLine(Canvas, J, B);
    Canvas.Pen.Color := clRed;
    PaintLine(Canvas, I, B);
    PaintLine(Canvas, J, A);
    end;
    end;
    ---
    //2 empty parenthesis after Synchronize would be optional, as-in... Synchronize() do...

    My 0.02cents :)

    ReplyDelete
  16. Stevie,

    Interesting suggestion. In the future, we will be looking for ways to simplify the syntax.

    Allen.

    ReplyDelete
  17. Henrik,

    Just like I mentioned at the beginning of the post, "I’ll just leave it up to you whether or not these utility functions are useful,"

    Yes, for my very simple and contrived case, the "Using" technique is overkill. But that wasn't really the point.

    Allen.

    ReplyDelete
  18. Tjipke,

    "When the with goes out of scope, the interfaced object is freed, and it frees the contained object."

    Unless I'm mistaken, all temporary interface references are released at the end of the enclosing method, not when the with goes out of scope. There is only *one* case that I'm aware of where automatic cleanup occurs prior to the end of a method and that is the for..in..do syntax. In that case interfaces and enumerator objects are freed at the end of the loop.

    Allen.

    ReplyDelete
  19. Another Using for Object parameterless constructor:

    ...
    class procedure Using(Proc: TProc); static;
    ...

    class procedure Obj.Using(Proc: TProc);
    var
    o: T;
    begin
    o := T.Create;
    try
    Proc(O);
    finally
    O.Free;
    end;
    end;



    So you can call without creating object

    Obj.Using(procedure(o: TStringList)
    begin
    o.Add('Hello');
    end);



    My 2 Cents

    ReplyDelete
  20. Now, I'm not on a crusade against anonymous methods, really I'm not. I'm keeping my eyes open, really, trust me :-).

    First, I'm not familiar with TMonitor (it apparently is part of the Delphi Parallel Library). The online help doesn't really give a clue. Maybe that needs a bit of updating? :-)

    <quote>
    Description
    This is Enter, a member of class TMonitor.
    </quote>

    But, given this example, I cannot see the added value compared to the 'traditional' implementation like:

    TMonitor.Enter(ListBox1);
    try
    ListBox1.Items.Add ('One');
    ListBox1.Items.Add ('Two');
    finally
    TMonitor.Exit (ListBox1);
    end;

    It might be that there's something about the DPL that I need to know before understanding the beauty of this example, but reading the code, I really don't see a point for an anonymous method. This is something that could be done using existing statements, without additional effort.

    To add to this though, I do see a bit of the elegance and at least this example is readable. I just don't see the added value for this particular case, sorry. But, if more than one statement is needed for creating the circumstances for a piece of arbitrairy code, I think this might help.

    ReplyDelete
  21. My problem with using generics/anon methods to add things to the language that could/should be added to the language is that it potentially results in a cacophony of implementations (that you need to understand the internal workings of each of, in order to appreciate the behaviour they introduce to the code you are reading).

    try..finally may be more verbose but it is also instantly recognisable, transparent and most importantly /universally consistent/ in a way that these (and possible alternative implementations achieving the same thing) are not.

    Oxygene's "using" syntax meets all three of these criteria (recognisability of course comes only with familiarity, but that is itself more likely since it is part of the language).

    I sincerely hope that minor but very useful language improvements such as these will not now be fobbed off in future on the basis that "you can do it yourself using generics and anon methods".

    ReplyDelete
  22. So far all the anonymous methods samples I've seen look more like an evolved form of "with" code obfuscation, except with added hidden machinery so you can have more bugs than those mere obfuscation would introduce.

    Color me strongly not convinced these do anything to improve code readability and maintainability.

    ReplyDelete
  23. 1. For 'Using':

    Report No: 63369 (RAID: 261057) Status: Open
    Inline declaration of variables / "using" -like keyword
    http://qc.codegear.com/wc/qcmain.aspx?d=63369

    2. For 'lambdas' (aka "the better, smaller, cleaner, safer way to write the anon. routines" :-)) perhaps having,
    a.) the begin/end block optional would help, IOW why for if {condition} then {statement}, for {cycle} do {statement} etc. the begin/end is optional and for function Foo the begin/end is compulsory?
    b.) if the first keyword in lambda is 'function' then the compiler will "inject"/try to apply a 'Result:=' on the (compound) statement and will throw a syntax error is something is wrong.

    Hence we'll have:

    x:=2; y:=2;
    a:=function: double; x+y;

    - or - (of course you'll implement QC #63479 - Type inference, even if Barry says that the compiler must be adapted isn't? ;-) )

    a:=function: typed; x+y;

    FTR, personally I don't like

    a:=function: x+y;

    ...because it's error prone. This POV isn't mine, but I subscribe to it. We had a discussion in .non-tech some months ago where we settled all these. See the QC report for the details.

    ReplyDelete

Please keep your comments related to the post on which you are commenting. No spam, personal attacks, or general nastiness. I will be watching and will delete comments I find irrelevant, offensive and unnecessary.