reflection based on my experience so far on compile-time meta-programming in D as a novice user: the problems

H. S. Teoh hsteoh at quickfur.ath.cx
Tue Sep 15 00:41:12 UTC 2020


On Mon, Sep 14, 2020 at 11:17:09PM +0000, mw via Digitalmars-d wrote:
[...]
> https://github.com/mingwugmail/talibd

Yes, I managed to find this link.


[...]
> bool TA_MA(double[] inData , double[] outMA , int
> MA_optInTimePeriod=default_MA_optInTimePeriod, TA_MAType
> optInMAType=default_optInMAType) { ... }
> 
> bool TA_RSI(double[] inData , double[] outRSI , int
> RSI_optInTimePeriod=default_RSI_optInTimePeriod) { ... }
> 
> bool TA_MACD(double[] inData , double[] outMACD, double[] outMACDSignal,
> double[] outMACDHist , int optInFastPeriod=default_optInFastPeriod, int
> optInSlowPeriod=default_optInSlowPeriod, int
> optInSignalPeriod=default_optInSignalPeriod) { ... }
[...]
> These are the target functions we want to generate.

If you already have these declarations, what's the purpose of
"generating" them?  Just curious if there is a reason for this, or this
is just some arbitrary decision?


> Now the goal: write a single function template to generate these 3
> functions in D, better not to use the very raw string interpolation
> :-)
[...]

Honestly, if what you want is to generate separate function
declarations, then why *not* just use string interpolation?  That's
essentially what you're trying to do, after all: you have a list of
function names, and some scheme for generating the parameter lists.  So,
generate the parameter list as a string, then mixin.  Mission
accomplished.  I don't understand this phobia of string mixins -- they
were included in D precisely for this sort of use case!

Or is the point to unify everything into a single function call? If so,
based on the description on this page:

	https://www.ta-lib.org/d_api/d_api.html#Output%20Size

which describes a specific parameter structure with inter-relations
between them, I'd say, the way I'd do it is to slurp the entire argument
list into a single variadic parameter, and handle the breakdown of the
parameter groups manually (beats trying to coax the compiler to do
something it isn't expecting to be doing).

Furthermore, based on the description in the above-linked page, there
are double/float overloads you wish to support, which can be handled by
another compile-time parameter.

So I'd start with something like this:

	bool TA(string funcName, Float, Args...)(Args args)
		if (is(Float == float) || is(Float == double))
	{
		// to be filled in, see below
		...
	}

You'd call it something like this:

	auto result = TA!"TA_MA"( /* arguments go here */ );

Slurping the entire argument list as a general blob of arguments gives
us the flexibility to do whatever we wish with it, like optional
parameters in the middle of the list, relationships between the numbers
of subgroups of parameters, and what-not.  Sounds like what we have
here, so that's how I'd do it.

So now, what do with do with the arguments we received? According to the
above linked page, we have a list of start/end indices, which I assume
should come in pairs? (Correct me if I'm wrong.)  At first I thought the
number of pairs must match the number of input arrays, but apparently
I'm wrong, based on your examples above?  Anyway, for now I'm just going
to assume the number of pairs must match the number of inputs.  So then
it would look something like this:

	bool TA(string funcName, Float, Args...)(Args args)
		if (is(Float == float) || is(Float == double))
	{
		enum nPairs = NumPairs!Args;
		enum arraysStart = 2*nPairs;

		// This part is optional, it's just to type-check that
		// the caller passed the right number of arguments
		static foreach (i; 0 .. nArrays)
		{
			static assert(is(Args[arraysStart + i] == Float[]),
				/* Optional: add nice error message */);
		}

		// To get at the array contents: write:
		//	args[arraysStart + i]
		// where i is the index.

		... // more to follow
	}

	// Helper template
	template NumPairs(Args...) {
		static if (Args.length <= 1)
			enum NumPairs = 0;
		else static if (!is(Args[0] == int) || !is(Args[0] == int))
			enum NumPairs = 0;
		else
			enum NumPairs = 2 + NumPairs(Args[2..$]);
	}

Next, you have a bunch of optional parameters. I'm not sure exactly what
determines what these parameters are or how, but let's assume there's
some template that, given the name of the target function, tells us (1)
the names of these parameters and (2) their types.  So we have:

	bool TA(string funcName, Float, Args...)(Args args)
		if (is(Float == float) || is(Float == double))
	{
		enum nPairs = NumPairs!Args;
		enum nArrays = nPairs;
		enum arraysStart = 2*nPairs;

		// This part is optional, it's just to type-check that
		// the caller passed the right number of arguments
		static foreach (i; 0 .. nArrays)
		{
			static assert(is(Args[arraysStart + i] == Float[]),
				/* Optional: add nice error message */);
		}

		// To get at the array contents: write:
		//	args[arraysStart + i]
		// where i is the index.

		enum optionsStart = arraysStart + nArrays;
		enum nOptions = NumberOfOptions!funcName;

		// Optional: type check
		alias OptTypes = OptionTypes!funcName;
		static foreach (i, T; OptTypes)
		{
			static assert(is(Args[optionsStart + i] == OptTypes[i]),
				/* Optional: nice error message here */);
		}

		// To get at the options, write:
		//	args[optionsStart + i]
		// where i is the index.

		... // more to follow
	}

Next, we have two fixed output parameters, which follow the optional
parameters. So something like this:

	bool TA(string funcName, Float, Args...)(Args args)
		if (is(Float == float) || is(Float == double))
	{
		enum nPairs = NumPairs!Args;
		enum nArrays = nPairs;
		enum arraysStart = 2*nPairs;

		// This part is optional, it's just to type-check that
		// the caller passed the right number of arguments
		static foreach (i; 0 .. nArrays)
		{
			static assert(is(Args[arraysStart + i] == Float[]),
				/* Optional: add nice error message */);
		}

		// To get at the array contents: write:
		//	args[arraysStart + i]
		// where i is the index.

		enum optionsStart = arraysStart + nArrays;
		enum nOptions = NumberOfOptions!funcName;

		// Optional: type check
		alias OptTypes = OptionTypes!funcName;
		static foreach (i, T; OptTypes)
		{
			static assert(is(Args[optionsStart + i] == OptTypes[i]),
				/* Optional: nice error message here */);
		}

		// To get at the options, write:
		//	args[optionsStart + i]
		// where i is the index.

		// Output parameters
		enum outStart = optionsStart + nOptions;
		int* outBegIdx = args[outStart];
		int* outNbElement = args[outStart + 1];

		... // more to follow
	}

Finally, we have the list of output arrays, which presumably must match
the number of input arrays. So:

	bool TA(string funcName, Float, Args...)(Args args)
		if (is(Float == float) || is(Float == double))
	{
		enum nPairs = NumPairs!Args;
		enum nArrays = nPairs;
		enum arraysStart = 2*nPairs;

		// This part is optional, it's just to type-check that
		// the caller passed the right number of arguments
		static foreach (i; 0 .. nArrays)
		{
			static assert(is(Args[arraysStart + i] == Float[]),
				/* Optional: add nice error message */);
		}

		// To get at the array contents: write:
		//	args[arraysStart + i]
		// where i is the index.

		enum optionsStart = arraysStart + nArrays;
		enum nOptions = NumberOfOptions!funcName;

		// Optional: type check
		alias OptTypes = OptionTypes!funcName;
		static foreach (i, T; OptTypes)
		{
			static assert(is(Args[optionsStart + i] == OptTypes[i]),
				/* Optional: nice error message here */);
		}

		// To get at the options, write:
		//	args[optionsStart + i]
		// where i is the index.

		// Output parameters
		enum outStart = optionsStart + nOptions;
		int* outBegIdx = args[outStart];
		int* outNbElement = args[outStart + 1];

		// Output arrays
		enum outArraysStart = outStart + 2;

		// Optional type check
		static foreach (i; 0 .. nArrays)
		{
			static assert(is(Args[outArraysStart + i] == Float[]),
				/* Optional: add nice error message */);
		}

		// To get at the output arrays, write:
		//	args[outArraysStart + i]
		// where i is the index.

		... // more to follow
	}

Now we're ready to actually implement the function.  Presumably we're
not just handing a bunch of stuff over to a C function -- because for
that, a string mixin would've sufficed instead of this entire charade.
The whole point of even doing any of the above is so that you can write
D code that takes advantage of type information in Args and generic
iteration over (subsets of) it.

Since I've no idea what these functions are supposed to be doing, I'm
just going to assume there's some template function called Impl that,
given some one set of start/end indices, an input array, and an output
array, will do whatever it is that needs to be done. So:

	bool TA(string funcName, Float, Args...)(Args args)
		if (is(Float == float) || is(Float == double))
	{
		enum nPairs = NumPairs!Args;
		enum nArrays = nPairs;
		enum arraysStart = 2*nPairs;

		// This part is optional, it's just to type-check that
		// the caller passed the right number of arguments
		static foreach (i; 0 .. nArrays)
		{
			static assert(is(Args[arraysStart + i] == Float[]),
				/* Optional: add nice error message */);
		}

		// To get at the array contents: write:
		//	args[arraysStart + i]
		// where i is the index.

		enum optionsStart = arraysStart + nArrays;
		enum nOptions = NumberOfOptions!funcName;

		// Optional: type check
		alias OptTypes = OptionTypes!funcName;
		static foreach (i, T; OptTypes)
		{
			static assert(is(Args[optionsStart + i] == OptTypes[i]),
				/* Optional: nice error message here */);
		}

		// To get at the options, write:
		//	args[optionsStart + i]
		// where i is the index.

		// Output parameters
		enum outStart = optionsStart + nOptions;
		int* outBegIdx = args[outStart];
		int* outNbElement = args[outStart + 1];

		// Output arrays
		enum outArraysStart = outStart + 2;

		// Optional type check
		static foreach (i; 0 .. nArrays)
		{
			static assert(is(Args[outArraysStart + i] == Float[]),
				/* Optional: add nice error message */);
		}

		// To get at the output arrays, write:
		//	args[outArraysStart + i]
		// where i is the index.

		// Finally, do the actual computation:
		bool result;
		foreach (i; 0 .. nArrays)
		{
			Impl!funcName(
				args[2*i .. 2*i+2],	// start/end indices
				args[arraysStart + i],	// input array
				args[optionsStart .. optionsStart + nOptions], // optional arguments
				outBegIdx, outNbElement,
				args[outArraysStart + i], // output arrays
			);

			result = ...; // do whatever's needed here
		}

		return result;
	}

The implementation of `Impl` is left as an exercise for the reader. ;-)
So there we have it.

Now, if this whole charade is *really* just to call some C functions in
the most pointlessly convoluted way, then you'd just replace the last
part of this function with something like this:

	bool TA(string funcName, Float, Args...)(Args args)
		if (is(Float == float) || is(Float == double))
	{
		...

		// See? I told you, you should have just used a string
		// mixin from the get-go instead of this pointless
		// charade.  Of course, the charade does win you the
		// ability to type-check these things, but so does
		// mixing in the generated string declarations.
		return mixin(funcName ~ "(" ~
			"args[2*i .. 2*i+2], "~
			"args[arraysStart + i],	"~
			"args[optionsStart .. optionsStart + nOptions], "~
			"outBegIdx, outNbElement, "~
			"args[outArraysStart + i]);");
	}

Well OK, I omitted something, that is, mapping the D arrays to C
pointers.  You can just map them to their respective .ptr values with
staticMap if you wish.  Or just:

	iota(0, nArrays).map!(i => format("&args[arraysStart + %d].ptr", i)).joiner.array

Which again, shows that the whole thing is kinda pointless if all you
want to do with it at the end is to call some C function.  The
metaprogramming machinery is really for when you want to write actual D
code where you can access the args[] array at compile-time and do useful
stuff with it.  After all, this is *D* metaprogramming we're talking
about; the code needs to be in D in order to benefit from it.  If all
you want is to generate a bunch of C declarations, the C processor does
its job very well, and so do string mixins.

Use the right tool for the right job.


T

-- 
Why did the mathematician reinvent the square wheel?  Because he wanted to drive smoothly over an inverted catenary road.


More information about the Digitalmars-d mailing list