digitalmars.D - Producing nicer template errors in D libraries
- H. S. Teoh (67/67) Apr 10 2012 A lot of template code (e.g. a big part of Phobos) use signature
- Nick Sabalausky (1/1) Apr 10 2012 Clever, I like it :)
- bearophile (5/7) Apr 10 2012 See also:
- Jacob Carlborg (13/23) Apr 11 2012 Original I would have gone with something like:
- Don Clugston (24/69) Apr 11 2012 This is the way we used to do it, before we had template constraints.
- Andrei Alexandrescu (3/13) Apr 11 2012 This would be a major improvement to the compiler.
- Steven Schveighoffer (16/27) Apr 11 2012 I'd go further. I'd like to see else if as well.
- Andrei Alexandrescu (8/14) Apr 11 2012 I advocated this to Walter and he talked me out of it.
- Steven Schveighoffer (24/42) Apr 11 2012 How so? The if/else if/else is used to find a template to match within ...
A lot of template code (e.g. a big part of Phobos) use signature constraints, for example: void put(T,R)(R range, T data) if (isOutputRange!R) { ... } This is all nice and good, except that when the user accidentally calls .put on a non-range, you get reams and reams of compiler errors complaining that certain templates don't match, certain other instantiations failed, etc., etc.. Which is very unfriendly for newbies who don't speak dmd's dialect of encrypted Klingon. (And even for seasoned Star Trek^W^W I mean, D fans, it can take quite a few seconds before the real cause of the problem is located.) So I thought of a better way of doing it: void put(T,R)(R range, T data) { static if (isOutputRange!R) { ... // original code } else { static assert(0, R.stringof ~ " is not an output range"); } } This produces far more readable error messages when an error happens. But it also requires lots of boilerplate static if's for every function that currently uses isOutputRange in their signature constraint. void put(T,R)(R range, T data) if (assertIsOutputRange!R) { ... } template assertIsOutputRange(R) { static if (isOutputRange!R) enum assertIsOutputRange = true; else static assert(0, R.stringof ~ " is not an output range"); } Now we can stick assertIsOutputRange everywhere there was a signature constraint before, without needing to introduce lots of boilerplate code. But what if there are several overloads of the same function, each with different signature constraints? For example: int func(T)(T arg) if (constraintA!T) { ... } int func(T)(T arg) if (constraintB!T) { ... } int func(T)(T arg) if (constraintC!T) { ... } If constraintA asserts, then the compiler will not compile the code even if the call actually matches constraintB. So for cases like this, we introduce a catchall overload: int func(T)(T arg) if (!constraintA!T && !constraintB!T && !constraintC!T) { static assert(0, "func can't be used for type " ~ T.stringof ~ " because <insert some excuse here>"); } Now the compiler will correctly resolve the template to instantiate, while still providing a nice error message for when nothing matches. What do y'all think of this idea? (Personally I think it's really awesome that D allows you to customize compiler errors using static assert, and we should be taking advantage of it much more. I propose doing this at strategic places in Phobos, esp. where you'd otherwise get errors from 5 levels deep inside some obscure nested template that hardly anybody understands how it's related to the original failing instantiation (e.g. a no-match error from appendArrayWithElemImpl instantiated from appendToArrayImpl instantiated from nativeArrayPutImpl instantiated from arrayPutImpl instantiated from putImpl instantiated from put -- OK I made that up, but you get the point).) T -- Amateurs built the Ark; professionals built the Titanic.
Apr 10 2012
H. S. Teoh:except that when the user accidentally calls .put on a non-range, you get reams and reams of compiler errorsSee also: http://d.puremagic.com/issues/show_bug.cgi?id=7878 Bye, bearophile
Apr 10 2012
On 2012-04-10 21:45, H. S. Teoh wrote:A lot of template code (e.g. a big part of Phobos) use signature constraints, for example: void put(T,R)(R range, T data) if (isOutputRange!R) { ... } This is all nice and good, except that when the user accidentally calls .put on a non-range, you get reams and reams of compiler errors complaining that certain templates don't match, certain other instantiations failed, etc., etc.. Which is very unfriendly for newbies who don't speak dmd's dialect of encrypted Klingon. (And even for seasoned Star Trek^W^W I mean, D fans, it can take quite a few seconds before the real cause of the problem is located.)Original I would have gone with something like: struct OutputRange { void foo (); void bar (); } void put(T,OutputRange R)(R range, T data) Or: void put(T,R : OutputRange)(R range, T data) Something like that. -- /Jacob Carlborg
Apr 11 2012
On 10/04/12 21:45, H. S. Teoh wrote:A lot of template code (e.g. a big part of Phobos) use signature constraints, for example: void put(T,R)(R range, T data) if (isOutputRange!R) { ... } This is all nice and good, except that when the user accidentally calls .put on a non-range, you get reams and reams of compiler errors complaining that certain templates don't match,void put(T,R)(R range, T data) if (assertIsOutputRange!R) { ... } template assertIsOutputRange(R) { static if (isOutputRange!R) enum assertIsOutputRange = true; else static assert(0, R.stringof ~ " is not an output range"); } Now we can stick assertIsOutputRange everywhere there was a signature constraint before, without needing to introduce lots of boilerplate code. But what if there are several overloads of the same function, each with different signature constraints? For example: int func(T)(T arg) if (constraintA!T) { ... } int func(T)(T arg) if (constraintB!T) { ... } int func(T)(T arg) if (constraintC!T) { ... } If constraintA asserts, then the compiler will not compile the code even if the call actually matches constraintB. So for cases like this, we introduce a catchall overload: int func(T)(T arg) if (!constraintA!T&& !constraintB!T&& !constraintC!T) { static assert(0, "func can't be used for type " ~ T.stringof ~ " because<insert some excuse here>"); } Now the compiler will correctly resolve the template to instantiate, while still providing a nice error message for when nothing matches.This is the way we used to do it, before we had template constraints. Although it works OK in simple cases, it doesn't scale -- you need to know all possible template constraints. I would like to see something in the language conceptually like: int func(T)(T arg) else { ... } for a template which is instantiated only if all constraints have failed. 'default' is another keyword which could be used, and 'if(false)' is another, but else is probably more natural. Any attempt to instantiate an 'else' template always results in an error, just as now. (in practice: if instantiating the else template didn't trigger a static assert, a generic error message is issued) It is an error for there to be more than one matching 'else' template.What do y'all think of this idea? (Personally I think it's really awesome that D allows you to customize compiler errors using static assert, and we should be taking advantage of it much more. I propose doing this at strategic places in Phobos, esp. where you'd otherwise get errors from 5 levels deep inside some obscure nested template that hardly anybody understands how it's related to the original failing instantiation (e.g. a no-match error from appendArrayWithElemImpl instantiated from appendToArrayImpl instantiated from nativeArrayPutImpl instantiated from arrayPutImpl instantiated from putImpl instantiated from put -- OK I made that up, but you get the point).)Definitely. Incidentally, when all template constraints fail, the compiler could check them all again, and tell you exactly which conditions failed... Algorithm: We know that: false = !constraint1() && !constraint2() && !constraint3(). break each constraints into top-level boolean expressions. Then simplify (possibly using a BDD). easy (but common) example, if constraint1() = !A() && B(), constraint2 = !A() && C(), constraint3() == !A() && !B() && !D() it simplifies to: false = !A(). So we generate an error only saying that !A() failed.
Apr 11 2012
On 4/11/12 6:42 AM, Don Clugston wrote:Incidentally, when all template constraints fail, the compiler could check them all again, and tell you exactly which conditions failed... Algorithm: We know that: false = !constraint1() && !constraint2() && !constraint3(). break each constraints into top-level boolean expressions. Then simplify (possibly using a BDD). easy (but common) example, if constraint1() = !A() && B(), constraint2 = !A() && C(), constraint3() == !A() && !B() && !D() it simplifies to: false = !A(). So we generate an error only saying that !A() failed.This would be a major improvement to the compiler. Andrei
Apr 11 2012
On Wed, 11 Apr 2012 07:42:54 -0400, Don Clugston <dac nospam.com> wrote:This is the way we used to do it, before we had template constraints. Although it works OK in simple cases, it doesn't scale -- you need to know all possible template constraints. I would like to see something in the language conceptually like: int func(T)(T arg) else { ... }I'd go further. I'd like to see else if as well. Currently, you have to repeat conditions from previous template constraints: int func(T)(T arg) if (constraint1) {...} int func(T)(T arg) if (!constraint1 && constraint2) It's like writing a large if sequence without the benefit of else. Sometimes you even need to put && !constraint2 in the first version.for a template which is instantiated only if all constraints have failed. 'default' is another keyword which could be used, and 'if(false)' is another, but else is probably more natural. Any attempt to instantiate an 'else' template always results in an error, just as now. (in practice: if instantiating the else template didn't trigger a static assert, a generic error message is issued)Why go this far? Why can't you have an else that's instantiated? Essentially, you are still forcing this sequence: int func(T)(T arg) if(constraint) {...} int func(T)(T arg) if(!constraint) {...} when the second line could just be: int func(T)(T arg) else {...} I don't see the benefit of enforcing the else branch to give an error. -Steve
Apr 11 2012
On 4/11/12 9:23 AM, Steven Schveighoffer wrote:Essentially, you are still forcing this sequence: int func(T)(T arg) if(constraint) {...} int func(T)(T arg) if(!constraint) {...} when the second line could just be: int func(T)(T arg) else {...} I don't see the benefit of enforcing the else branch to give an error.I advocated this to Walter and he talked me out of it. Essentially template constraints help choosing the right overload given the arguments. Just like overloading, such selection should proceed across modules. If we have an "else" template we give up on that approach. Besides, it's extremely rare that a template works with an open-bounded set of types. Andrei
Apr 11 2012
On Wed, 11 Apr 2012 10:33:26 -0400, Andrei Alexandrescu <SeeWebsiteForEmail erdani.org> wrote:On 4/11/12 9:23 AM, Steven Schveighoffer wrote:How so? The if/else if/else is used to find a template to match within that module. It doesn't affect other modules. Right now, all the if statements from all modules are combined. This wouldn't change that. For example, you have: if(module1.constraint1) matches++; if(module2.constraint1) matches++; if(module2.constraint2 && !module2.constraint1) matches++; This then becomes: if(module1.constraint1) matches++; if(module2.constraint1) matches++; else if(module2.constraint2) matches++; In other words, else is shorthand for "and doesn't match any other previous constraints in this module". It looks pretty DRY to me... I don't see how this affects ambiguity between modules at all.Essentially, you are still forcing this sequence: int func(T)(T arg) if(constraint) {...} int func(T)(T arg) if(!constraint) {...} when the second line could just be: int func(T)(T arg) else {...} I don't see the benefit of enforcing the else branch to give an error.I advocated this to Walter and he talked me out of it. Essentially template constraints help choosing the right overload given the arguments. Just like overloading, such selection should proceed across modules. If we have an "else" template we give up on that approach.Besides, it's extremely rare that a template works with an open-bounded set of types.Maybe, but you can rely on the template not compiling in those cases. -Steve
Apr 11 2012