| |
| Posted by H. S. Teoh | PermalinkReply |
|
H. S. Teoh
| Given the volume of negative posts about D around these parts, I thought I'd offer some counterpoint: why I love D. I'm planning to post this every now and then, as a sort of informal series.
Today, I grappled with the following situation: I have a bunch of functions with parameters that perform certain operations, that I want to expose to the user via the program's command-line arguments. Also, for maximum user-friendliness, the program's help text should accurately describe each function (treated as a subcommand on the command line) and what parameters each takes.
The traditional solution, of course, is simple: write a showHelp() function that prints some text describing each function and its parameters, then in main() write a big switch statement over function name, and each case block parses the program arguments, converts them to a suitable form, and invokes the function.
It's not a bad solution, but involves a lot of tedious work that, because of their repetitious nature, is liable to human error. Every time I added a new function, I have to change two other places in the code (update the help text, add a new case block). Raise your hand, whoever has encountered out-of-sync/outdated help texts in programs. :-P
So I sought for a better solution, in the spirit of DRY: put said functions into a wrapper struct, mark them static, and add a string UDA that will be introspected by showHelp() to generate a list of functions that's guaranteed to be up-to-date, and by main() to generate the requisite case blocks.
The help text part is simple: just introspect the wrapper struct, extract the function name, the string UDA for the description, and the list of parameter names, print them out. Easy.
The case blocks are a little trickier. It would have been simple if every function had the same set of parameters: then it's just a matter of mixing in the function name in a standard call. But what if each function had a *different* set of parameters?
My initial attempt was to iterate over the parameters and generate code for parsing each one, assign them to temporary variables, then create a list containing the list of temporary variables to use as a mixin to pass them all to the target function. Would work, but would involve a lot of hairy, ugly code.
And then inspiration struck: why do I need to individually declare those variables (and invent names for each one)? I don't need to. I can get a hold of the function's parameters as a parameter tuple using `is(typeof(func) Params : __parameters)`, then declare a compound variable with this parameter tuple as its type:
void main(string[] args) {
...
static if (is(typeof(myfunc) Params : __parameters)) {
Params funcArgs; // use a single name for ALL function arguments
// Now populate the arguments by converting command-line
// arguments to the right types:
foreach (i, T; Params) {
// std.conv.to is Da Bomb
import std.conv : to;
funcArgs[i] = args[i+2].to!T;
}
// To call the function, we just pass the entire
// aggregate to it in one shot:
myfunc(funcArgs); // thanks to the magic tuple
// type, this auto-expands
// funcArgs into multiple
// arguments
}
}
The compound variable 'funcArgs' is a reification of the target function's arguments; this allows us to manipulate it like a pseudo-array in the loop that parses command-line arguments. We don't need to construct any mixins involving cumbersome parameter lists! (Of course, in the actual code a mixin is still needed in order to bind `myfunc` to the actual target function, which is iterated over by name.)
I couldn't even begin to imagine how to pull off something like this in C++... for sure, it will be NOWHERE near as elegant as the above.
D r0x0rs!!!
T
--
War doesn't prove who's right, just who's left. -- BSD Games' Fortune
|