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 , December 1, 2010

interface
uses
SysUtils, Generics.Collections;

type
TSwitchProc = reference to procedure;
TGenericSwitch = class(TObjectDictionary)
private
FTheElseCase: TSwitchProc;
FTheTargetKey: KeyType;
function AddSwitchCase(const name: KeyType;
const value: TSwitchProc): TGenericSwitch;
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;
function ElseCase(const Action: TSwitchProc): TGenericSwitch;
procedure EndCase;
property Cases[const name:KeyType; const value:TSwitchProc]: TGenericSwitch
read AddSwitchCase; default;
end;

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

implementation

{ TGenericSwitch }

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

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

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

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

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

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

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

{ TStringSwitch }

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


end.

10 thoughts on “A generic case for strings

  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. 🙂

    Like

  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…

    Like

  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.

    Like

  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). 🙂

    Like

  5. @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? 😉

    Like

  6. 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.

    Like

Leave a Reply

Please log in using one of these methods to post your comment:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Google photo

You are commenting using your Google account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s

This site uses Akismet to reduce spam. Learn how your comment data is processed.