Delphi Programming

and software in general.

Wednesday, December 1, 2010

A generic case for strings

Do you remember the discussion about a case statement for strings?

I got this flash idea after reading Jolyon Smith's "The case for case[]", and remembering a comment from Francisco Ruiz on Nick Hodges' article on THTMLWriter which suggested using a default array property in a creative fashion.

Honestly, it is not really a true case statement, and it might not be as fast as an if then else, but here is how it looks when used. A bit ugly. but good fun :)

program TestGenericsSwitch;
{$apptype Console}
uses
  GenericsSwitch;
begin
  TStringSwitch.CaseOf('chARLie')
    ['Any', procedure begin
            Writeln('Definitively any case');
          end]
    ['B', procedure begin
            Writeln('B all you can B');
          end]
    ['Charlie', procedure begin
            Writeln('Checkpoint C');
          end]
    .ElseCase(procedure begin
            Writeln('Else what?');
          end)
    .EndCase;
end.

And here is how it is implemented.

unit GenericsSwitch;

/// Written by Lars Fosdal <lars@fosdal.com>, December 1, 2010

interface
uses
  SysUtils, Generics.Collections;

type
  TSwitchProc = reference to procedure;
  TGenericSwitch<KeyType> = class(TObjectDictionary<KeyType, TSwitchProc>)
  private
    FTheElseCase: TSwitchProc;
    FTheTargetKey: KeyType;
    function AddSwitchCase(const name: KeyType; 
                           const value: TSwitchProc): TGenericSwitch<KeyType>;
    procedure SetTheElseCase(const Value: TSwitchProc);
    procedure SetTheTargetKey(const Value: KeyType);
  protected
    function ValidateKey(Key:KeyType):KeyType; virtual;
    property TheTargetKey:KeyType read FTheTargetKey write SetTheTargetKey;
    property TheElseCase:TSwitchProc read FTheElseCase write SetTheElseCase;
  public
    class function CaseOf(const Key: KeyType):TGenericSwitch<KeyType>;
    function ElseCase(const Action: TSwitchProc): TGenericSwitch<KeyType>;
    procedure EndCase;
    property Cases[const name:KeyType; const value:TSwitchProc]: TGenericSwitch<KeyType>
                  read AddSwitchCase; default;
  end;

  TStringSwitch = class(TGenericSwitch<String>)
    function ValidateKey(key:String):String; override;
  end;

implementation

{ TGenericSwitch<KeyType, TSwitchProc> }

function TGenericSwitch<KeyType>.AddSwitchCase(const name: KeyType; const value: TSwitchProc): TGenericSwitch<KeyType>;
begin
  Result := Self;
  Add(ValidateKey(Name), Value);
end;

class function TGenericSwitch<KeyType>.CaseOf(const Key: KeyType): TGenericSwitch<KeyType>;
begin
  Result := Create;
  Result.TheTargetKey := Key;
end;

function TGenericSwitch<KeyType>.ElseCase(const Action: TSwitchProc): TGenericSwitch<KeyType>;
begin
  Result := Self;
  TheElseCase := Action;
end;

procedure TGenericSwitch<KeyType>.EndCase;
var
  DoIt : TSwitchProc;
begin
  if TryGetValue(TheTargetKey, DoIt)
  then DoIt
   else
   if Assigned(TheElseCase)
    then TheElseCase;
  Destroy;
end;

procedure TGenericSwitch<KeyType>.SetTheElseCase(const Value: TSwitchProc);
begin
  FTheElseCase := Value;
end;

procedure TGenericSwitch<KeyType>.SetTheTargetKey(const Value: KeyType);
begin
  FTheTargetKey := ValidateKey(Value);
end;

function TGenericSwitch<KeyType>.ValidateKey(Key: KeyType):KeyType;
begin
  Result := Key;
end;

{ TStringSwitch }

function TStringSwitch.ValidateKey(key: String): String;
begin
  Result := LowerCase(Key);
end;


end.

9 comments:

  1. fwiw - I think the fact that you *can* do this (and have done) is interesting and clever.

    But seriously, we should not have to use such ugly and cumbersome constructs - these things should be added directly to the language.

    The fact that we can work around these gaps in the language is no excuse. We shouldn't have to.

    If anything, I think the resulting mess that arises from using these new language features demonstrates better than any reasoned argument why using them to plug such gaps is such a bad idea.

    It just smells bad. Very bad.

    But, as I say, sincerely - very neat, and really very clever. I just won't let it anywhere near my code. :)

    ReplyDelete
  2. Very neat! I confess I had to read it a few times before I followed the implementation though. I kept on thinking the core 'trick' was something to do with the use of generics or anonymous methods, when it was really to do with a good ol' default property...

    ReplyDelete
  3. @Jolyon - It isn't as dirty as it first looks. It will crash on duplicates, but that is "desirable" and in theory can be safeguarded against. The syntax is reasonably solid, and with a little tinkering, it could be used as a "reusable" case statement, ie create once, and call multiple times, instead of like now, where it destroys itself after each use.

    @Gad - I agree it should be a language feature to have string case statements.

    @Chris - I first did a string only variation, without the "ElseCase", which simply tested as the statements were added, if they matched the key, and stopped processing AddSwitchCases after a match was found. Then I thought that if I make it generic, I could allow using any type as the case id (case MethodPointer of, etc.).

    It would look even cleaner if the anonymous code syntax would allow code fragments (one-liners, or begin/end blocks). Then there is the issue with not being able to assign result values to the outer methdod, directly from within the anon.method.

    ReplyDelete
  4. Lars, it doesn't much matter how dirty it actually it is... it just looks dirty. If it's actually "clean" under the hood, if anything that makes its apparent dirtiness less excusable, not more. imho.

    FYI - it inspired me to create a "Pure Pascal" alternative (actually, I should say it reminded me of an old technique that I have regurgitated for hopefully a new audience). :)

    ReplyDelete
  5. oops - I should have added: "See my latest blog post".

    But I'm hoping/guessing you would have stumbled across it anyway. :)

    ReplyDelete
  6. Lars, it is very bad to steal my idea!!!

    April 2010

    See
    http://santonov.blogspot.com/2010/04/just-any-type-delphi-case-statement.html

    You may find my more smart method to support subtyping on case elements

    https://forums.embarcadero.com/message.jspa?messageID=229522

    ReplyDelete
  7. @Jolyon - I would probably have found your post through Delphifeeds.

    @Sergey - My bad! I hadn't seen your implementation. Although they both share similar features, like the default property use, they still are quite different. But what can I say? Great minds think alike? ;)

    ReplyDelete
  8. Another point do this, instead of using StringToIndex and case, is that this method is resiliant regards to adding, removing or reordering cases. With StringToIndex, you have to "manually" maintain coherence between the string and the case index. How many have been burned by that little detail? I know I have.

    ReplyDelete
  9. I think a dispatch table is a more elegant solution.

    Dispatch table in Delphi

    ReplyDelete