www.digitalmars.com         C & C++   DMDScript  

digitalmars.D - Producing nicer template errors in D libraries

reply "H. S. Teoh" <hsteoh quickfur.ath.cx> writes:
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.

So here's take #2:

	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
next sibling parent "Nick Sabalausky" <a a.a> writes:
Clever, I like it :) 
Apr 10 2012
prev sibling next sibling parent "bearophile" <bearophileHUGS lycos.com> writes:
H. S. Teoh:

 except that when the user accidentally calls
 .put on a non-range, you get reams and reams of compiler errors

See also: http://d.puremagic.com/issues/show_bug.cgi?id=7878 Bye, bearophile
Apr 10 2012
prev sibling next sibling parent Jacob Carlborg <doob me.com> writes:
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
prev sibling next sibling parent reply Don Clugston <dac nospam.com> writes:
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,

 So here's take #2:

 	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
next sibling parent Andrei Alexandrescu <SeeWebsiteForEmail erdani.org> writes:
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
prev sibling parent Andrei Alexandrescu <SeeWebsiteForEmail erdani.org> writes:
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
prev sibling next sibling parent "Steven Schveighoffer" <schveiguy yahoo.com> writes:
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
prev sibling parent "Steven Schveighoffer" <schveiguy yahoo.com> writes:
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:
 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.

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.
  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