Thread overview
Producing nicer template errors in D libraries
Apr 10, 2012
H. S. Teoh
Apr 10, 2012
Nick Sabalausky
Apr 10, 2012
bearophile
Apr 11, 2012
Jacob Carlborg
Apr 11, 2012
Don Clugston
April 10, 2012
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.
April 10, 2012
Clever, I like it :)


April 10, 2012
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
April 11, 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
April 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,

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



April 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


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


April 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:
>> 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