Thursday, November 12, 2009

Power Tip: 'With'-Helpers


In the previous article, I mentioned that Delphi includes a 'with' statement, and although it seems like an unimportant for a language to have, I'm going to teach you something that will make you stand back and give you an alternate perspective on this simple mechanism. Sorry all you C++ fans, this power tip applies only to languages that have a 'with' statement.

With statements are the most useful for one thing: 'with'-helpers.

What is a 'with'-helper?

Yeah, I knew you were thinking that: a 'with'-helper is a class or data structure that is designed to be more convieniently used via a 'with' statement in Delphi. It's one of the most Ruby-like features that Delphi has to offer, brining a greater increase of convenience and fun to Delphi programming. The most quick way to demonstrate is through examples, so come on, let's take a look...

Examples

Let's start with a simple example: a work-around for variables. In Dephi, one normally has to explicitly declare variables before the block of code that uses them begins, like so:
procedure ReportMyError(Code: integer);
var
  //we're declaring the variable here in the "var" block
  vMsg: string;
begin
  if Code > 0 then
    if GetErrorMsgStr(Code, vMsg) then
      PrintLn('Error!'#13#10 + vMsg);
end;

This can quickly become inconvenient, so, why don't we create a 'with'-helper that provides a temporary string variable:
type
  TStringValue = record
    Value: string;
  end;

function StringValue: TStringValue;
begin
  Result.Value := '';
end;

And then use it like so:
procedure ReportMyError(Code: integer);
begin
  if Code > 0 then
    //calling the StringValue() function to create a new string value,
    //which is put into the context with the "with" statement:
    with StringValue do
      if GetErrorMsgStr(Code, Value) then
        PrintLn('Error!'#13#10 + Value);
end;

Did you notice where identifier called 'Value' came from? 'Value' was implicitly referenced from the TStringValue record returned by the StringValue() call and implicitly put in scope by the 'with' statement. Get all that? Thanks to Delphi's built-in support for records, we don't do any memory management! Yay! Delphi is better!

We can do much better than this, however. Let's create a nice 'with'-helper for providing the effect of a 'let' statement: create and compute a value, sort of like a closure:
type
  TFloatValue = record
    Value: extended;
  end;

function FloatValue(AValue: extended): TFloatValue;
begin
  Result.Value := AValue;
end;

Now we use it:
function EmitRatios(A, B: extended);
begin
  with FloatValue(Sqrt(Sqr(A)+Sqr(B))) do
    begin
      PrintLn(FloatToStr(Value/A));
      PrintLn(FloatToStr(Value/B));
    end;
end;

Sweet! The value passed to FloatValue() was only computed once! Yay for the extra efficiency boost! Records can do some good and come in handy-dandy using them. Along with what we've seen, we can also return multiple results from functions, capture them with a 'with' statement, and leverage them thus. But there's one problem with this last example: records can only do so much: their fields are mutable and cannot be protected from writing to them. This could cause horrible bugs in a program if we're not careful. Thus, we move on up to using classes as 'with'-helpers. Here's our previous example modified:
type
  TFloatValue = class
  private
    FValue: extended;
  public
    property Value: extended read GetValue;
  end;

function FloatValue(AValue: extended): TFloatValue;
begin
  Result := TFloatValue.Create;
  Result.FValue := AValue;
end;

//using it:

function EmitRatios(A, B: extended);
begin
  with FloatValue(Sqrt(Sqr(A)+Sqr(B))) do
    try
      PrintLn(FloatToStr(Value/A));
      PrintLn(FloatToStr(Value/B));
    finally
      Free;
    end;
end;

Notice the try-finally block for freeing the created class called by and provided for the 'with' statement. Delphi knows that the structure used by the 'with' statement should have its members in the enclosing scope, so this technique will also work even if the 'with' statement is already inside a class method.

Now that we're using classes, we can do more sophisticated stuff, seeing as classes provide not only properties, but methods. As a last example:
type
  TTokens = array of TToken;

procedure Append(AToken: TToken; var Tokens: TTokens);
begin
  SetLength(Tokens, Succ(Length(Tokens)));
  Tokens[High(Tokens)] := AToken;
end;

function ScanTokens(AStr: string): TTokens;
begin
  SetLength(Result, 0);
  //constructs a TScanner class, giving it the 
  //string to scan: TScanner = token iterator
  with Scanner(AStr) do
    try
      while ReadNextToken do //TScanner.ReadNextToken method
        Append(CurToken, Result); //TScanner.CurToken property
    finally
      Free;
    end;
end;

Conclusion

Thanks for learning. Hope you enjoyed this power tip. Happy coding!

No comments:

Post a Comment