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.

Reply via email to