Let's suppose I wrote the following template function:
import std.meta;
enum bool isString(T) = is(T == string);
void foo(Args...)(auto ref Args args)
if (!anySatisfy!(isString, Args)) {
// ...
}
This one is variadic, but it could as well have been
non-variadic. The important
aspect is that it has a constraint. In this case, the constraint
is that it should
accept any argument types *but* strings.
Now, if I call it with a string argument:
foo(1, "a");
I get the following error:
file(line): Error: template foo cannot deduce function from
argument types !()(int, string), candidates are:
file(line): foo(Args...)(auto ref Args arg) if
(!anySatisfy!(isString, Args))
Ok, so the call does not compile, but the message is rather
vague: it doesn't
tell me which argument(s) failed to satisfy the constraint.
In this simple example it's easy to see where the error is, but
if foo() was
called in a generic way (i.e. arguments come from somewhere else,
their type
determined by inference, etc.), or if the constraint was more
complex, it
wouldn't be as easy to spot.
So, to help with this, let me write a checker and modify foo's
signature, thanks
to CTFE:
template types(args...) {
static if (args.length)
alias types = AliasSeq!(typeof(args[0]),
types!(args[1..$]));
else
alias types = AliasSeq!();
}
auto noStringArgs(args...)() {
import std.format;
// use types, as otherwise iterating over args may not
compile
foreach(i, T; types!args) {
static if (is(T == string)) {
pragma(msg, format!"Argument %d is a string, which
is not supported"
(i+1));
return false;
}
}
return true;
}
void foo(Args...)(auto ref Args args)
if (noStringArgs!args) {
// ...
}
Now if I call foo() with a string argument, I get this:
foo(1, "a");
Argument 2 is a string, which is not supported
file(line): Error: template foo cannot deduce function from
argument types !()(int, string), candidates are:
file(line): foo(Args...)(auto ref Args arg) if
(noStringArgs!args)
That's a little bit better: if foo() fails to compile, I get a
hint on which
argument is incorrect. However, as you probably can tell, this
doesn't scale. If
later I decide to provide an overload for foo() that *does*
accept string
arguments, I'm going to see that message every time a call to
foo() is made.
What if we allowed constraint expressions, in addition to a type
convertible to
bool, return a Tuple!(Bool, Msgs), where Bool is convertible to
bool, and Msgs
is a string[]?
Then my checker could be implemented like this:
auto noStringArgs(args...)() {
import std.format;
import std.typecons;
string[] errors;
foreach(i, T; types!args) {
static if (is(T == string)) {
errors ~= format!"Argument %d is a string"(i+1));
}
}
if (errors) return tuple(false, ["This overload does not
accept string arguments"] ~ errors);
return tuple(true, errors.init);
}
So it would accumulate all concrete error messages for the
signature, and prefix them with a general descriptive message.
When resolving overloads, the compiler could collect strings from
such tuples,
and if the resolution (or deduction, in case of single overload)
fails,
print them as error messages:
foo(1, "a", 3, "c");
file(line): Error: template foo cannot deduce function from
argument types !()(int, string), candidates are:
file(line): foo(Args...)(auto ref Args arg) if
(noStringArgs!args):
file(line): This overload does not accept string arguments
file(line): Argument 2 is a string, which is not supported
file(line): Argument 4 is a string, which is not supported
And in case of overloads:
auto noNumericArgs(args...)() {
import std.format;
import std.typecons;
import std.traits : isNumeric;
string[] errors;
foreach(i, T; types!args) {
static if (isNumeric!T) {
errors ~= format!"Argument %d (%s) is a string"(i+1,
T.stringof));
}
}
if (errors) return tuple(false, ["This overload does not
accept numeric arguments"] ~ errors);
return tuple(true, errors.init);
}
void foo(Args...)(auto ref Args args)
if (noStringArgs!args) { /* ... */ }
void foo(Args...)(auto ref Args args)
if (!noStringArgs!args && noNumericArgs!args) { /* ... */ }
foo(1, 2); // ok, no error, first overload
foo("a", "b"); // ok, no error, second overload
foo(1, "b", "c"); // error
file(line): Error: template foo cannot deduce function from
argument types !()(int, string), candidates are:
file(line): foo(Args...)(auto ref Args arg) if
(noStringArgs!args):
file(line): This overload does not accept string arguments
file(line): Argument 2 is a string
file(line): Argument 3 is a string
file(line): foo(Args...)(auto ref Args arg) if
(!noStringArgs!args && noNumericArgs!args):
file(line): This overload does not accept numeric arguments
file(line): Argument 1 (int) is numeric
This would clearly show exactly for what reason each overload
failed. You can
imagine for complex template functions (i.e. likes of
std.concurrency.spawn, std.getopt,
etc) this could help convey the error much more concisely than
just saying
"hey, I failed, here are the candidates, go figure it out...".
A crude implementation of this is possible as a library:
https://dpaste.dzfl.pl/0ba0118c3cd9
but without language support, it'll just riddle the compiler
output with
messages on every call, regardless of the success of overload
resolution,
so the only use for that would be in case of no overloads. And
the messages
are ordered before compiler errors, which is less than helpful.
Another idea, instead of using tuples, introduce a stack of
messages for each
overload, and allow a special pragma during constraint evaluation:
bool noStringArgs(args...)() {
import std.format;
import std.typecons;
foreach(i, T; types!args) {
static if (is(T == string)) {
pragma(overloadError, format!"Argument %d is a
string"(i+1)));
// may return early or continue to collect all errors
// return false;
}
}
return true;
}
pragma(overloadError, string) will "push" an error onto message
stack.
After evaluating noStringArgs!args, the compiler would check the
stack, and if
it's not empty, discard the result (consider it false) and use
the strings from that stack
as error messages.
Trying to call noStringArgs() outside of constraint evaluation
would result in
compiler error (pragma(overloadError, string) should only be
available in that
context).
There are other alternatives, e.g. there's a DIP by Kenji Hara:
https://wiki.dlang.org/User:9rnsr/DIP:_Template_Parameter_Constraint
The approach I'm proposing is more flexible though, as it would
allow to
evaluate all arguments as a unit and infer more information (e.g.
__traits(isRef, args[i]).
Constraint on every argument won't allow the latter, and would
potentially require writing more explicit overloads.
What do you guys think? Any critique is welcome, as well as
pointers to alternatives, existing discussions on the topic, etc.