Should this work?

H. S. Teoh hsteoh at quickfur.ath.cx
Thu Jan 9 16:50:57 PST 2014


On Thu, Jan 09, 2014 at 11:28:07PM +0000, Brad Anderson wrote:
> On Thursday, 9 January 2014 at 20:40:33 UTC, H. S. Teoh wrote:
> >On Thu, Jan 09, 2014 at 06:25:33PM +0000, Brad Anderson wrote:
> >>On Thursday, 9 January 2014 at 14:08:02 UTC, Manu wrote:
> >[...]
> >>>On a side note, am I the only one that finds
> >>>std.algorithm/std.range/etc for string processing really obtuse?  I
> >>>can rarely understand the error messages, so say it's better than
> >>>STL is optimistic.
> >>
> >>I absolutely hate the "does not match any template declaration"
> >>error. It's extremely unhelpful for figuring out what you need to do
> >>and anytime I try to do something fun with ranges I can expect to
> >>see it a dozen times.
> >
> >Yeah, that error drives me up the wall too. I often get screenfuls of
> >errors, dumping 25 or so overloads of some obscure Phobos internal
> >function (like toImpl) as though an end-user would understand any of
> >it.  You have to parse all the sig constraints (and boy some of them
> >are obscure), *understand* what they mean (which requires
> >understanding how Phobos works internally), and *then* try to figure
> >out, by elimination, which is the one that you intended to match, and
> >why your code failed to match it.
> >
> >I'm almost tempted to say that using sig constraints to differentiate
> >between template overloads is a bad idea. Instead, consider this
> >alternative implementation of toImpl:
> >
> >	template toImpl(S,T)
> >		// N.B.: no sig constraints here
> >	{
> >		static if (... /* sig constraint conditions for overload #1 */)
> >		{
> >			S toImpl(T t)
> >			{
> >				// implementation here
> >			}
> >		}
> >		else static if (... /* sig constraint conditions for overload #2
> >*/)
> >		{
> >			S toImpl(T t)
> >			{
> >				// implementation here
> >			}
> >		}
> >		...
> >		else // N.B.: user-readable error message
> >		{
> >			static assert(0, "Unable to convert " ~
> >				T.stringof ~ " to " ~ S.stringof);
> >		}
> >	}
> >
> >By putting all overloads inside a single template, we can give a
> >useful default message when no overloads match.
> >
> 
> Interesting and there is a lot of flexibility there. It does make
> the functions a lot more verbose though for something that is really
> the compiler's job (clearly describing errors).

The way I see it, is that any sig constraints should go on the outer
template, and should define the *logical* scope of all overloads
encompassed therein. E.g., if you have a set of overloads for sqrt, say,
then the outer template would have a sig constraint that matches any
number-like type. Within the template, each individual overload would
handle various concrete types, and the static assert at the end is
essentially saying "in theory your arguments should match *something* in
this template, but currently your particular combination of types isn't
implemented by any overload".

Or, put another way, the outer template represents the "logical"
meta-function that does some given task (e.g., sqrt computes the square
root of *something*), whereas the inner overloads provide the actual set
of available implementations that implement that meta-function (computes
the square root of an int, computes the square root of a float, etc.).
My hypothesis is that you get the wall-of-template-errors problem when
there's a logical meta-function (or a small number of them) that, for
implementational reasons, consists of a large set of overloads. By
treating the logical meta-function as an actual entity (the outer
template), we can give a unified error message of failure to implement
the meta-function for the requested types, rather than many error
messages for each of the many overloads, most of which are irrelevant to
the user.


> >Alternatively, maybe sig constraints can have an additional string
> >parameter that specifies a message that explains why that particular
> >overload was rejected. These messages are not displayed if at least
> >one overload matches; only if no overload matches, they will be
> >displayed (so that the user can at least see why each of the
> >overloads didn't match).
> >
> 
> Each constraint would have a string? I think that would help for
> some of the more obscure constraints that aren't wrapped up in an
> eponymous template helper but I don't think it'd help with the
> problem generally because the problem is identifying which exact
> constraint failed.

True.


> Example:
> 
>     void main()
>     {
>       import std.algorithm, std.range;
>       struct A { }
>       auto a = recurrence!"n"(0).take(5).find(A());
>     }
> 
> This is the error message you get:
> 
> ---
> /d14/f101.d(5): Error: template std.algorithm.find does not match
> any function template declaration. Candidates are:
> /opt/compilers/dmd2/include/std/algorithm.d(3650):
> std.algorithm.find(alias pred = "a == b", R, E)(R haystack, E
> needle) if (isInputRange!R &&
> is(typeof(binaryFun!pred(haystack.front, needle)) : bool))
> /opt/compilers/dmd2/include/std/algorithm.d(3713):
> std.algorithm.find(alias pred = "a == b", R1, R2)(R1 haystack, R2
> needle) if (isForwardRange!R1 && isForwardRange!R2 &&
> is(typeof(binaryFun!pred(haystack.front, needle.front)) : bool) &&
> !isRandomAccessRange!R1)
> /opt/compilers/dmd2/include/std/algorithm.d(3749):
> std.algorithm.find(alias pred = "a == b", R1, R2)(R1 haystack, R2
> needle) if (isRandomAccessRange!R1 && isBidirectionalRange!R2 &&
> is(typeof(binaryFun!pred(haystack.front, needle.front)) : bool))
> /opt/compilers/dmd2/include/std/algorithm.d(3821):
> std.algorithm.find(alias pred = "a == b", R1, R2)(R1 haystack, R2
> needle) if (isRandomAccessRange!R1 && isForwardRange!R2 &&
> !isBidirectionalRange!R2 && is(typeof(binaryFun!pred(haystack.front,
> needle.front)) : bool))
> /opt/compilers/dmd2/include/std/algorithm.d(4053):
> std.algorithm.find(alias pred = "a == b", Range, Ranges...)(Range
> haystack, Ranges needles) if (Ranges.length > 1 &&
> is(typeof(startsWith!pred(haystack, needles))))
> ---
> 
> Where do you even begin with that flood of information? To fix it
> all you really want to see is which constraint you didn't satisfy.
> An error message like this would help greatly:
> 
> ---
> /d539/f571.d(5): Error: template std.algorithm.find call fails all
> constraints. Candidates are:
> /opt/compilers/dmd2/include/std/algorithm.d:
>   (3650) find(alias pred = "a == b", R, E)(R haystack, E needle):
>               isInputRange!R
>            && is(typeof(binaryFun!pred(haystack.front, needle)) :
> bool) <- FAILS
>   (3713) find(alias pred = "a == b", R1, R2)(R1 haystack, R2
> needle):
>               isForwardRange!R1
>            && isForwardRange!R2 <- FAILS
>            && is(typeof(binaryFun!pred(haystack.front,
> needle.front)) : bool)
>            && !isRandomAccessRange!R1
>   (3749) find(alias pred = "a == b", R1, R2)(R1 haystack, R2
> needle):
>               isRandomAccessRange!R1 <- FAILS
>            && isBidirectionalRange!R2
>            && is(typeof(binaryFun!pred(haystack.front,
> needle.front)) : bool)
>   (3821) find(alias pred = "a == b", R1, R2)(R1 haystack, R2 needle)
>               isRandomAccessRange!R1 <- FAILS
>            && isForwardRange!R2
>            && !isBidirectionalRange!R2
>            && is(typeof(binaryFun!pred(haystack.front,
> needle.front)) : bool)
>   (4053) find(alias pred = "a == b", Range, Ranges...)(Range
> haystack, Ranges needles)
>               Ranges.length > 1 <-- FAILS
>            && is(typeof(startsWith!pred(haystack, needles)))

But still, this will dump out a whole bunch of overloads that aren't
necessarily interesting to the user. I mean, if I want to search for an
int in an int[], but accidentally passed a string instead of an int,
then I'm really only interested in seeing how the overload that handles
int[] searching failed to match my string argument; I don't care about
why the sig constraints failed for the overload that handles linked
lists, for example.

Perhaps a better solution lies in distinguishing the logical scope of
the function, vs. requirements on its argument types within that scope.
For example, the find() overload that searches T[] for some T, has T[]
as its scope, whereas within this scope, it imposes certain requirements
on the needle U (e.g., U must be comparable with an element of T). This
suggests that it should be implemented like this:

	auto find(R,S)(R haystack, S needle)
		if (is(R == T[], T)) // <-- defines the scope of this function
	{
		static if (isComparable(S, ElementType!R)) // <-- Defines type requirements within this function's scope
		{
			// implementation here
		}
		else static if (isComparable(ElementType!S, ElementType!R)) // <-- ditto
		{
			// implementation here
		}
		else
			static assert(0, "Don't know how to search for "
				~ S.stringof ~ " in " ~ R.stringof);
	}

Then when you try to search for a string in an int[], for example, it
will first match this overload of find(), then fail the static if
conditions because the needle you passed in doesn't match the type
requirements for searching an int[].

Note that I've grouped at least two of the current find() overloads
under a single overload above -- because they are just two
implementations for handling two cases within the same scope: searching
an array. The fact that array-searching is implemented by two distinct
algorithms is irrelevant to the user, and so it makes sense to "hide"
them inside a single function's body.

So to summarize:
(1) use sig constraints to define the scope of an overload; and
(2) use static if inside the function body (or template body) to enforce
type requirements within that scope.

This solves the problem of needing the compiler to somehow read your
mind and figure out exactly which of the 56 overloads of find() you
intended to match but failed to.


T

-- 
The only difference between male factor and malefactor is just a little emptiness inside.


More information about the Digitalmars-d mailing list