digitalmars.D.learn - Adding to a global AliasSeq
- repr_man (129/129) Aug 14 2023 Consider the following template mixin:
Consider the following template mixin: ```d mixin template Base() { int x(){ return 10; } } ``` It could be used in a variety of structs as follows: ```d struct Child { mixin Base!(); } ``` Now, let's suppose we write a function with a parameter that should only be able to take items that mix in `Base`: ```d auto f(T)(T arg) { return arg.x; } ``` This works because D uses structural subtyping. However, this creates a problem: if we make another struct that looks like it mixes in `Base` and pass it to the function, we could get unexpected results: ```d struct Other { int x = 5; } unittest { import std.stdio; auto c = Child(); auto o = Other(); writeln(f(c)); writeln(f(o)); } ``` The output from running `dub test` is as follows: ``` 10 5 ``` Even worse, because of UFCS, if there happens to be a function with the name `x` in scope, it could get called: ```d auto x(int arg) { import std.stdio; writeln("Hello world"); return arg; } unittest { import std.stdio; auto c = Child(); auto o = Other(); auto i = 7; writeln(f(c)); writeln(f(o)); writeln(f(i)); } ``` The output from running `dub test` is the following: ``` 10 5 Hello world 7 ``` Obviously, this case is fairly trivial; however, I could see it becoming more of a problem in large projects, where an import could pull something into the namespace and there would be no compiler error about the function (in this case, `f`) being used incorrectly. The problem could be partially solved by enforcing that no one uses UFCS (through using regular call syntax and fully qualifying all functions), but this is ultimately not a sustainable solution, as it involves all contributing parties knowing about and using this convention. And, if anyone makes a mistake, it would be difficult to find. A better solution would be to make some sort of list of structs that mix in `Base`: ```d alias BaseUsers = AliasSeq!(Child); ``` Then, `f` could have a constraint added: ```d auto f(T)(T arg) if(staticIndexOf!(T, BaseUsers) != -1) { return arg.x; } ``` Now, `dub test` fails properly: ``` Error: template `f` is not callable using argument types `!()(Other)` Candidate is: `f(T)(T arg)` with `T = Other` must satisfy the following constraint: ` staticIndexOf!(T, BaseUsers) != -1` Error: template `f` is not callable using argument types `!()(int)` Candidate is: `f(T)(T arg)` with `T = int` must satisfy the following constraint: ` staticIndexOf!(T, BaseUsers) != -1` ``` The problem with this, of course, is that every user of `Base` must be known ahead of time and added to the list. Apart from being error-prone, it also prevents this approach from being used in a library. In "Fantasy D", I would want to do something like this in the template mixin: ```d mixin template Base() { BaseUsers ~= typeof(this); int x(){ return 10; } } ``` However, since `AliasSeq`s are immutable, and since there is no way that I know of to override a fully qualified alias, this does not work. Is there any template-fu of which I am not aware that would make the thing I am trying to do possible?
Aug 14 2023
On Tuesday, 15 August 2023 at 02:30:37 UTC, repr_man wrote:Consider the following template mixin: ```d mixin template Base() { int x(){ return 10; } } ``` It could be used in a variety of structs as follows: ```d struct Child { mixin Base!(); } ``` Now, let's suppose we write a function with a parameter that should only be able to take items that mix in `Base`: ```d auto f(T)(T arg) { return arg.x; } ``` This works because D uses structural subtyping. However, this creates a problem: if we make another struct that looks like it mixes in `Base` and pass it to the function, we could get unexpected results: ```d struct Other { int x = 5; } unittest { import std.stdio; auto c = Child(); auto o = Other(); writeln(f(c)); writeln(f(o)); } ``` The output from running `dub test` is as follows: ``` 10 5 ``` [...] Is there any template-fu of which I am not aware that would make the thing I am trying to do possible?You can add a kind of tag with the mixin, that will allow to to test if the type supplied to `f` is well a Base implementor: ```d mixin template Base() { enum LookImABase = true; int x(){ return 10; } } struct Child { mixin Base!(); } auto f(T)(T arg) if (is(typeof(T.LookImABase))) { return arg.x; } struct Other { int x = 5; } void main(string[] args) { import std.stdio; auto c = Child(); auto o = Other(); writeln(f(c)); // ok writeln(f(o)); // error } ``` This is what is called in D the "design by introspection". There are alternatives way of doing that, notably with UDAs. ```d mixin template Base() { enum Base; Base int x(){ return 10; } } ``` But the introspection code is more complex (std.traits getUDA, hasUDA) Finally a different approach, with no introspection, is to use a named mixin: ```d mixin template Base() { enum Base; int x(){ return 10; } } struct Child { mixin Base!() base; } auto f(T)(T arg) { return arg.base.x; } struct Other { int x = 5; } void main(string[] args) { import std.stdio; auto c = Child(); auto o = Other(); writeln(f(c)); writeln(f(o)); // error } ``` which works but has the same problem as what lead you to ask your question, for example if it turns out that the supplied argument has unfortunately a `base` member that itself has a `x` member.
Aug 15 2023
On Tuesday, 15 August 2023 at 07:02:45 UTC, Basile B. wrote:You can add a kind of tag with the mixin, that will allow to to test if the type supplied to `f` is well a Base implementor:Firstly, thank you for your help. This is probably the best solution. Secondly, I feel very stupid for not having come up with this by myself. Would you be willing to explain some of the thought process behind your solution? Are there any key factors that I should look for in future template-related problems? Or, if it just comes from experience and seeing this sort of thing repeatedly, that's fair, too. Thanks, once again!
Aug 15 2023
On Tuesday, 15 August 2023 at 12:20:54 UTC, repr_man wrote:On Tuesday, 15 August 2023 at 07:02:45 UTC, Basile B. wrote:This is just classic D. The way this works is tied to overload-sets resolution. You have two functions templates, same name, same count of parameters. The only way to make both exist in the set is "template constraint", one works given certain static conditions, the other not. Also, as it's time for the explanations, one thing about that system and that you should keep in mind is that it's costly, i.e "can cause major slowdown of compilation".You can add a kind of tag with the mixin, that will allow to to test if the type supplied to `f` is well a Base implementor:Firstly, thank you for your help. This is probably the best solution. Secondly, I feel very stupid for not having come up with this by myself. Would you be willing to explain some of the thought process behind your solution? Are there any key factors that I should look for in future template-related problems? Or, if it just comes from experience and seeing this sort of thing repeatedly, that's fair, too. Thanks, once again!
Aug 18 2023