View mode: basic / threaded / horizontal-split · Log in · Help
April 10, 2012
Producing nicer template errors in D libraries
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
Re: Producing nicer template errors in D libraries
Clever, I like it :)
April 10, 2012
Re: Producing nicer template errors in D libraries
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
Re: Producing nicer template errors in D libraries
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
Re: Producing nicer template errors in D libraries
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
Re: Producing nicer template errors in D libraries
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
Re: Producing nicer template errors in D libraries
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
Re: Producing nicer template errors in D libraries
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
Re: Producing nicer template errors in D libraries
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
Top | Discussion index | About this forum | D home