Looks like wrong error message

H. S. Teoh hsteoh at quickfur.ath.cx
Mon Jan 29 00:08:17 UTC 2018


On Sun, Jan 28, 2018 at 10:53:39PM +0000, welkam via Digitalmars-d-learn wrote:
> On Sunday, 28 January 2018 at 20:42:52 UTC, Jonathan M Davis wrote:
[...]
> > However, you're not going to get an error message that says anything
> > like "the arguments aren't the same type." The compiler doesn't
> > understand what the template constraint means in "human terms." It
> > just knows whether it's true or false, and in this case, if you
> > provide arguments that don't have a common type that they implicitly
> > convert to, then the template constraint will fail. But ultimately,
> > you're going to have to read the template constraint and figure out
> > why the arguments are failing.
[...]

Well, that's not *quite* true.  The compiler *does* have enough
information to be able evaluate the constraints, for one.  So in theory
it *should* be able to identify which constraint(s) failed.  Some time
ago there was a push to refactor Phobos sig constraints in CNF
(conjunctive normal form), which would simplify this somewhat: if a
function has sig constraints X, Y and Z, and Y evaluated to false, then
the compiler could print condition Y as the cause of failure.

Furthermore, the reason for the perceived unhelpful message is more
subtle. It's because a sig constraint amounts to a limitation in scope
of a particular overload, effectively saying "this overload only applies
if conditions X, Y and Z are true". So a failure in one or more sig
constraints isn't necessarily a *problem* per se; it just means that
particular overload declines instantiation to the rest of the overload
set.  Only if the failure ultimately leads to a failure to match *all*
overloads, can we say for sure that the failed constraint was the
"cause" of the mismatch.

And this is where things get tricky, because if you have an overload of,
say, 10 members, then obviously a failure to find a match means that
*every* overload had its sig constraints fail one way or another. But
without being able to read the programmer's mind, the compiler has no
idea which overload was the *intended* target of the function call; so
it would not be able to tell which failed sig constraint was the most
relevant to the user.  All >=10 failed conditions would have to be
displayed, and it would be up to the user to decide which one is the
relevant one.

Of course, even in spite of this, displaying the failed conditions is
still better than nothing.  If the compiler assumes CNF on sig
constraints, then it can list, along with each candidate overload, the
failing term(s) in the CNF for that particular overload as the reason
that overload wasn't chosen. For example, the output could be something
like:

	Error: unable to match func() with argument type X; candidates
	are:
	std/range/package.d(100): auto func(R)(R r) if (isInputRange!R
	&& is(ElementType!R == char): condition is(ElementType!R ==
	char) failed
	std/range/package.d(200: auto func(R)(R r) if (isForwardRange!R
	&& is(ElementType!R : int): condition isForwardRange!R failed
	std/range/package.d(300): auto func(E)(E[] arr): X does not
	match parameter type E[]

You'd still have to parse through each listed overload, but at least the
compiler would tell you which sig constraint failed. IME, this would be
helpful, because sometimes it's not at all obvious which sig constraint
failed, especially if the sig constraints call upon other templates to
test the incoming type (e.g., isInputRange, which contains a whole bunch
of tests, any of which could fail and it's not always obvious which
ones), and I wind up having to parse through *all* sig constraints of
*all* overloads just to figure out which one was the real cause.


> I would not complain if there were multiple functions just like error
> said.  But the set contains only one making it not a set anymore and
> it says nothing about constraints.
[...]

This topic has come up again numerous times, and I think *some*
improvement on this front is necessary.

One improvement is to decrease our reliance on overloads, and to use
more permissive sig constraints with static ifs inside the function body
that can provide a more helpful error message. Not to mention, some of
the current Phobos overloads are divided based on implementation details
user code shouldn't need to know about, rather than *logical* overload
boundaries, for example:

	// handle the case when S is a string
	auto func(R,S)(R haystack, S needle)
	if (isInputRange!R && isSomeString!S) ...

	// handle the case when S is an array but not a string
	auto func(R,S)(R haystack, S needle)
	if (isInputRange!R && !isSomeString!S && isArray!S) ...

This is leaking implementation details into the sig constraints; it
really should be written this way instead:

	auto func(R,S)(R haystack, S needle)
	if (isInputRange!R)
	{
		static if (isSomeString!S)
			... // handle the case where S is a string
		else static if (isArray!S)
			... // handle the case where S is an array but not a string
		else
			// Helpful message if implementation doesn't
			// support the incoming type
			static assert(0, "S must be either a string or an array");
	}

This way, there's (1) a smaller number of overloads the user needs to
parse through when an error occurs; (2) when an error occurs, it's
obvious that the failing condition is isInputRange!R: the user only
needs to figure out 1 sig constraint as opposed to 4 to find the
problem; and (3) the else-clause of an internal static if can be used to
provide more user-friendly error messages, if the argument is an input
range, but not of a kind the current implementation supports.

I'd even venture to say that this applies not just to Phobos but to
template code in general.  Personally, I go by the rule that the sig
constraints should be general, and should document what the user would
logically understand the function parameters ought to be (i.e., the
parameter must be an input range). Anything else, like handling various
specializations (do X if the element type is char, do Y if it's a POD,
etc.) should be done inside the function body, away from the
public-facing sigs.  If the current implementation doesn't handle all of
the types that the function logically should, then the else-clause of
the internal static if can be used to provide a more user-friendly error
message explaining why the passed-in type doesn't work.


T

-- 
We are in class, we are supposed to be learning, we have a teacher... Is it too much that I expect him to teach me??? -- RL


More information about the Digitalmars-d-learn mailing list