Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Struct composition with mixin and templates

I can compose an AB struct that has all the members of structs A and B:

template AFields() {int a;}
struct A { mixin AFields; }
template BFields() {int b;}
struct B { mixin BFields; }
struct AB { mixin AFields; mixin BFields; }
A a; a.a = 1;
B b; b.b = 2;
AB ab; ab.a = 3; ab.b = 4;

But how can I construct AB, if I don't have control over A and B and I don't have AFields and BFields? I.e. how to write the CatStruct template so the code below compiles?

struct A { int a; }
struct B { int b; }
mixin CatStruct!("AB", A, B);
AB ab;
ab.a = 1; ab.b = 2;
like image 595
Tamas Avatar asked Nov 30 '22 00:11

Tamas


1 Answers

The standard library has a few hidden jewels that I didn't actually even know about myself until I peeked at the source to answer this question:

http://dlang.org/phobos/std_traits.html#Fields

and the ones right under it too. With these, we can make your CatStruct fairly succinctly. Behold:

mixin template CatStruct(string name, T...) { 
    static import std.traits, std.conv; 
    private string _code_generator() { 
        string code = "struct " ~ name ~ " {"; 
        foreach(oidx, t; T) { 
            foreach(idx, field; std.traits.FieldTypeTuple!t) 
                 // this line is a monster, see the end of this answer
                code ~= "std.traits.FieldTypeTuple!(T["~std.conv.to!string(oidx)~"])["~std.conv.to!string(idx)~"] "~ std.traits.FieldNameTuple!t[idx] ~ ";"; 
        } 
        code ~= "}"; 
        return code; 
    } 
    mixin(_code_generator()); 
} 

This uses a string mixin though... and while string mixins can do basically anything, they also basically suck. This is liable to be brittle but I think it will basically work while basically sucking.

It also won't do struct methods, but I think that's too hard to realistically do with any of these magical things, except perhaps opDispatch, as seen in the other answer (which is pretty nice btw, don't take my answer as a repudiation of that one, just another idea).

If there's clashing names between the two structs too, they will break this, and you will get a hideously ugly error message out of the compiler. With a real template mixin, there's an easy fix for that - a named template mixin, which allows you to disambiguate. But no such thing here. I guess you could hack one in if you needed it.

But anyway, there might be a way to use those FieldTypeTuple and FieldNameTuple from the stdlib to do this even nicer, but I think it is more-or-less what you're asking for now.

BTW, I'd say just do ordinary composition if you at all can, it is going to work the best in general. (Don't forget about alias this too which can do automatic forwarding to member variables.)


If you haven't done a lot of mixins, you probably want to ask my why I used that crazy string in the code ~= part instead of the more straightforward. code ~= field.stringof ~ " "~ FieldNameTuple!t[idx] ~ ";";

tl;dr: just trust me, ALWAYS use local names available to the scope where you run the mixin() itself in the code you generate. Long explanation follows/

It has to do with name clashes and symbol lookups. I used static imports and fully qualified names in the mixed in code - including using the local symbol for the FieldTypeTuple rather than field.stringof - to keep this as namespace-tidy as possible.

Consider the case where struct A imports some other module internally and defines a field with it.

// using my color.d just cuz I have it easily available
// but it could be anything, so don't worry about downloading it
struct A { import arsd.color; Color a; } 

AB ab; 
import arsd.color; 
ab.a = Color.white; ab.b = 2;  // we expect this work, should be the same type

Since that's a local import inside struct A, the name is meaningless at the point of the mixin.

Go ahead and adjust the mixin so it compiles using the simple line

                // comment fancy line
               // code ~= "std.traits.FieldTypeTuple!(T["~std.conv.to!string(oidx)~"])["~std.conv.to!string(idx)~"] "~ std.traits.FieldNameTuple!t[idx] ~ ";"; 

               // paste in simple line
                code ~= field.stringof ~ " "~ std.traits.FieldNameTuple!t[idx] ~ ";";

And compile:

$ dmd f.d ~/arsd/color.d
f.d-mixin-31(31): Error: undefined identifier 'Color' 
f.d(4): Error: mixin f.CatStruct!("AB", A, B) error instantiating 

Zoinks! It had no idea what the string "Color" was supposed to refer to. If we imported some other kind of struct Color in the local module, it would compile.... but then it would refer to a different type:

struct A { import arsd.color; Color a; } 
struct B { int b; } 
struct Color { static Color white() { return Color.init; } } 
mixin CatStruct!("AB", A, B);  

AB ab; 
import arsd.color; 
ab.a = Color.white; ab.b = 2; 

Compile it and see a silly sounding error:

$ dmd f.d ~/arsd/color.d
f.d(12): Error: cannot implicitly convert expression (white()) of type Color to Color

BTW: remember this if you ever see it in the wild - the compiler error message sounds absurd, "cannot implicitly convert Color to Color", but it actually does have a logical meaning: there's just two different types with the same name in different modules.

Anyway, it sounds silly, but makes sense because the two scopes imported different structs.

With the long-form FieldTypeTuple used with a local static import, it always refers to the actual type passed in. Indirectly, sure, but also unambiguously.

I apologize to those reading this who already know about the pitfalls of string mixins, but anyone finding this on a search might not know why I used that convoluted code. It is complex due to real world experience with actual problems, I swear! :) It is a lot easier to do it right the first time than try to debug the weird nonsense down the road it can bring doing it the other way.

like image 119
Adam D. Ruppe Avatar answered Dec 04 '22 12:12

Adam D. Ruppe