Why I love D: function parameter reification

H. S. Teoh hsteoh at qfbox.info
Wed Jun 8 21:47:16 UTC 2022


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


More information about the Digitalmars-d mailing list