digitalmars.D - Generic structural recursion
- H. S. Teoh (63/63) Jan 26 2022 Got stuck on this metaprogramming problem, and can't seem to find a way
- John Colvin (6/7) Jan 26 2022 You could define a template that introspects a struct type and
- H. S. Teoh (60/68) Jan 26 2022 Excellent idea!! Instead of trying to inject some arbitrary function
- =?UTF-8?Q?Ali_=c3=87ehreli?= (10/12) Jan 27 2022 I've done something similar for ROS where data is a bunch of bytes but
- Steven Schveighoffer (21/45) Jan 26 2022 This looks a lot like serialization. What you have done looks more
- H. S. Teoh (10/22) Jan 26 2022 That's what I had, but the problem is that the number of X arguments
- Steven Schveighoffer (16/36) Jan 27 2022 Well, this is what I would do then.
Got stuck on this metaprogramming problem, and can't seem to find a way to make it work, just wondering if any of you guys can come up with a clean solution: I have a bunch of nested structs of the form: struct S { double data; } struct T { S s1, s2; } struct U { S s, T[2] ts; } struct V { T t, U u; } ... // etc. and a bunch of functions that operate on structs of this form via structural recursion, of the following form (the exact number of parameters of type X differs across functions): -------- X interpolate(X)(ref X result, double idx, X data1, X data2) { foreach (field; FieldNameTuple!X) { alias Type = typeof(__traits(getMember, X, field)); if (is(Type == double)) __traits(getMember, result, field) = (1.0-idx)*__traits(getMember, data1, field) + idx*__traits(getMember, data2, field); else if (is(Type == struct)) { interpolate( __traits(getMember, result, field), idx, __traits(getMember, data1, field), __traits(getMember, data2, field)); } else if (is(Type == U[n], U, size_t n)) { foreach (i; 0 .. n) { interpolate( __traits(getMember, result, field)[i], idx, __traits(getMember, data1, field)[i], __traits(getMember, data2, field)[i]); } } } } -------- IOW, given some number of structs of type X, each function recurses down the nested structs and invokes some operation (in this case an interpolation function) on the corresponding double fields of each argument. As you can see above, there's a LOT of boilerplate needed for each function. Whenever I want to change the recursion (e.g., support dynamic arrays, or exclude certain types from the recursion) I have to modify every function by hand -- a tedious and error-prone process. I'd like to factor out the code that recurses down the nested structs into a separate template function, say call it structuralRecurse, and have each of the operations simply call structuralRecurse with some delegate or function literal that would be invoked on each set of corresponding double fields. But so far, I've not been able to do this in its full generality, i.e., something of the form: ------ void structuralRecurse(alias fun, T, size_t n)(T t0, T t1, ... T tn) { // invoke fun(t0.x.y.z, t1.x.y.z, ...); for each double field in T } ------ Even using a giant string mixin so far hasn't been obvious, because of the recursive descent required. And I'd like if possible to avoid using giant string mixins for obvious reasons. Anybody has any ideas? T -- If it breaks, you get to keep both pieces. -- Software disclaimer notice
Jan 26 2022
On Wednesday, 26 January 2022 at 18:22:18 UTC, H. S. Teoh wrote:[...]You could define a template that introspects a struct type and gives you a tuple of getters (each taking a struct and returning one of the doubles), then your functions that do real work would just loop over those getters. I suspect using the new(-ish) alias assign feature would help in defining that template.
Jan 26 2022
On Wed, Jan 26, 2022 at 06:41:50PM +0000, John Colvin via Digitalmars-d wrote:On Wednesday, 26 January 2022 at 18:22:18 UTC, H. S. Teoh wrote:Excellent idea!! Instead of trying to inject some arbitrary function with multiple arguments into the middle of a recursive traversal, do the traversal separately at compile-time exclusively on the type, generate an array of fields, and let the caller loop over them. Here's the solution I settled on: -------- string[] eachParam(T)(string prefix = "") { string[] result; if (__ctfe) { foreach (field; FieldNameTuple!T) { alias F = typeof(__traits(getMember, T, field)); static if (is(F == double)) { result ~= prefix ~ "." ~ field; } else static if (is(F == struct)) { result ~= eachParam!F(prefix ~ "." ~ field); } else static if (is(F == E[n], E, size_t n)) { foreach (j; 0 .. __traits(getMember, T, field).length) { result ~= eachParam!E(text(prefix, ".", field, "[", j, "]")); } } } return result; } else assert(0); } void interpolate(alias ipol = linearInterpolate, T) (ref T model, double idx, T t1, T t2) { static foreach (param; eachParam!T) { mixin("model" ~ param) = ipol(idx, mixin("pose1" ~ param), mixin("pose2" ~ param)); } } void nullify(ref T model) { static foreach (param; eachParam!T) { mixin("model" ~ param) = double.nan; } } // ... and so on -------- Both parts nice and clean. Well OK, so I used a bunch of mixins. But they are little self-contained snippets rather than entire function bodies. That's a win. T -- Why ask rhetorical questions? -- JC[...]You could define a template that introspects a struct type and gives you a tuple of getters (each taking a struct and returning one of the doubles), then your functions that do real work would just loop over those getters. I suspect using the new(-ish) alias assign feature would help in defining that template.
Jan 26 2022
On 1/26/22 10:41, John Colvin wrote:You could define a template that introspects a struct type and gives you a tuple of gettersI've done something similar for ROS where data is a bunch of bytes but with a well-defined structure. The difference there is array elements are inline with their length encoded first. So I introspected a FieldSkipper for each struct member which actually consisted of the FieldSkippers of the previous members (array skippers being smart and recursive to skip over the elements as well). Then I had a FieldGetter that would use the corresponding FieldSkipper to find the right bytes. Fun stuff... :) Ali
Jan 27 2022
On 1/26/22 1:22 PM, H. S. Teoh wrote:-------- X interpolate(X)(ref X result, double idx, X data1, X data2) { foreach (field; FieldNameTuple!X) { alias Type = typeof(__traits(getMember, X, field)); if (is(Type == double)) __traits(getMember, result, field) = (1.0-idx)*__traits(getMember, data1, field) + idx*__traits(getMember, data2, field); else if (is(Type == struct)) { interpolate( __traits(getMember, result, field), idx, __traits(getMember, data1, field), __traits(getMember, data2, field)); } else if (is(Type == U[n], U, size_t n)) { foreach (i; 0 .. n) { interpolate( __traits(getMember, result, field)[i], idx, __traits(getMember, data1, field)[i], __traits(getMember, data2, field)[i]); } } } } --------This looks a lot like serialization. What you have done looks more specific to your use case though. Why not call a function recursively for each type encountered? e.g.: ```d auto interpolate(alias fn, X : double)(ref X val1, ref X val2) { static if(is(typeof(fn(val1, val2)))) return fn(val1, val2); } auto interpolate(alias fn, X)(ref X val1, ref X val2) if (is(X == struct)) { // loop and recurse } ... ``` Then you can write a set of templates that act on the right types, and do the thing you want for that specific type, pass that in as `fn`. I find splitting the work into distinct portions when doing these kinds of introspection tasks to be more straightforward and testable. -Steve
Jan 26 2022
On Wed, Jan 26, 2022 at 02:06:18PM -0500, Steven Schveighoffer via Digitalmars-d wrote: [...]```d auto interpolate(alias fn, X : double)(ref X val1, ref X val2) { static if(is(typeof(fn(val1, val2)))) return fn(val1, val2); } auto interpolate(alias fn, X)(ref X val1, ref X val2) if (is(X == struct)) { // loop and recurse } ... ```That's what I had, but the problem is that the number of X arguments differs from function to function. So I'd have to duplicate the recursion part for each number of arguments, which is bad because there's a chance I might change the recursion in the unary version and forget to update it in the binary/ternary/etc. version. T -- Why can't you just be a nonconformist like everyone else? -- YHL
Jan 26 2022
On 1/26/22 2:57 PM, H. S. Teoh wrote:On Wed, Jan 26, 2022 at 02:06:18PM -0500, Steven Schveighoffer via Digitalmars-d wrote: [...]Well, this is what I would do then. ```d auto interpolateImpl(alias fn, Vals...)(ref Vals vals) if (is(Vals[0] == double)) { return fn(vals); } // handle all the other specific types etc. auto interpolate(alias fn, Vals...)(ref Vals vals) if (allSameType!Vals) { return interpolateImpl!fn(vals); } ``` Then the only complex one is the one for aggregates. -Steve```d auto interpolate(alias fn, X : double)(ref X val1, ref X val2) { static if(is(typeof(fn(val1, val2)))) return fn(val1, val2); } auto interpolate(alias fn, X)(ref X val1, ref X val2) if (is(X == struct)) { // loop and recurse } ... ```That's what I had, but the problem is that the number of X arguments differs from function to function. So I'd have to duplicate the recursion part for each number of arguments, which is bad because there's a chance I might change the recursion in the unary version and forget to update it in the binary/ternary/etc. version.
Jan 27 2022