Now that's a DIP that could use some love
Adam D. Ruppe
destructionator at gmail.com
Mon Sep 14 00:00:15 UTC 2020
On Sunday, 13 September 2020 at 22:29:00 UTC, Andrei Alexandrescu
wrote:
> Solves a long-standing, difficult problem of outputting
> meaningful, useful error messages.
Since this was written, the compiler's output has improved
significantly.
---
void foo(T)() if(true && is(T == string)) {}
void foo(T)() if(false && is(T == float)) {}
void main() { foo!int(); }
---
$ dmd cons
cons.d(4): Error: template cons.foo cannot deduce function from
argument types
(int)(), candidates are:
cons.d(1): foo(T)()
with T = int
must satisfy the following constraint:
is(T == string)
cons.d(2): foo(T)()
with T = int
must satisfy the following constraint:
false
What benefit would it bring adding a string to it? Instead of
`is(T == string)` it would say "T must be a string"? Not really
an improvement. Consider a case like `isInputRange!T`. Frequently
the question is: why isn't it considered an input range? The
answer might be "it is missing method popFront", and that would
help, but just rephrasing "must satisfy the following constraint:
isInputRange!T" as "T must be an input range" isn't a significant
improvement.
The block format's ability to unroll loops and provide a more
detailed message for individual items has potential to improve
the status quo in niche cases, but I'm not convinced this is a
good general solution. Should every constraint repeat the same
strings and always have to build it themselves? It doesn't even
seem possible to abstract the message to a library here. You
could have a condition and a message, as two separate functions,
but still work on the user side, every time they use it.
* * *
I think in my perfect world one of two things would happen:
1) You can use constraint functions that return a string or array
of strings. If this string is null, it *passes*. If not, the
returned string(s) is(are) considered the error message(s).
string checkInputRange(T)() {
if(!hasMember!(T, "popFront"))
return "missing popFront";
if(!hasMember!(T, "empty"))
return "must have empty";
if(!hasMember!(T, "front"))
return "must have front";
return null; // success
}
void foo(T)() if(checkInputRange!T) {}
foo!int;
with T = (int)
must satisfy the following constraint:
checkInputRange!T ("missing popFront");
This breaks backward compatibility since currently a null string
implicitly casts to boolean false. So it is the opposite behavior
right now. But it could perhaps be opt-in somehow.
It also addresses the loop cases of the DIP because the CTFE
helper check function can just loop over and build the more
detailed error messages that way.
It might be possible to do this with the DIP but it will take
some repetition:
void foo(T)() if(checkInputRange!T.passed,
checkInputRange!T.error) {}
Indeed, checkInputRange might return an object that has
opCast(T:bool)() { return this.errors == 0; }... but it still
must be repeated to get the message out.
While that would be possible, I believe a better DIP would be to
let the compiler recognize the pattern and work it automatically
for us. (And btw, __traits(compiles) might even return such a
CompileError object, with implicit cast to bool if no errors
occurred, and make the errors available for forwarding if they
did.)
2) Allow for `void` functions (or something) to be used in
constraints. If it throws an exception, the constraint fails and
the exception is used as the failing constraint error message. If
not, it is considered to pass.
It is currently illegal to use a void function as part of a
constraint, making it possible to ease into this without breaking
existing code.
Currently if a constraint calls a bool returning function and it
throws an exception, it actually does print the message! But it
also kills the whole compile.
---
bool check(T)() {
static if(is(T == float))
return true;
else
throw new Exception(T.stringof ~ " is not a
float");
}
void foo(T...)() if(check!T()) {}
void foo(T)() if(is(T == int)) {}
void main() {
foo!float();
foo!int();
}
---
cons.d(5): Error: uncaught CTFE exception object.Exception("int
is not a float")
cons.d(8): called from here: check()
Which is a pretty miserable state of affairs (that error message
doesn't even show the template instantiation point in user code!).
If we used exceptions for this purpose, it would just mean the
first overload fails, allowing the instance to use the second
overload.
CTFE check functions may also catch the exception and add
supplemental context through the usual mechanisms.
More information about the Digitalmars-d
mailing list