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.
fwiw - I think the fact that you *can* do this (and have done) is interesting and clever.
ReplyDeleteBut 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. :)
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@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.
ReplyDelete@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.
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.
ReplyDeleteFYI - 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). :)
oops - I should have added: "See my latest blog post".
ReplyDeleteBut I'm hoping/guessing you would have stumbled across it anyway. :)
Lars, it is very bad to steal my idea!!!
ReplyDeleteApril 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
@Jolyon - I would probably have found your post through Delphifeeds.
ReplyDelete@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? ;)
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.
ReplyDeleteI think a dispatch table is a more elegant solution.
ReplyDeleteDispatch table in Delphi