Jump to page: 1 24  
Page
Thread overview
September 13
https://github.com/dlang/DIPs/pull/131

In essence:

* Allow templates to accept multiple constraints.
* Each constraint accepts the syntax "if (expr1, expr2)" where expr2 is a string describing the requirement.
* The string corresponding to the constraint that fails is printed in the error message if no match is found.

Passes the elevator test. I can be done explaining it in 30 seconds to a current D programmer. In fact I just did.

Passes the laugh test. I can keep a straight face throughout, with or without a face mask.

Solves a long-standing, difficult problem of outputting meaningful, useful error messages.

Is small and integrates well with the language (per static assert and friends).
September 14
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.
September 14
On Monday, 14 September 2020 at 00:00:15 UTC, Adam D. Ruppe wrote:
> 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.

Here's a slight refinement: it does the cast to bool already. So we can return a struct with opCast. But how to get the message out?

It could be a magic type that the compiler recognizes...

Or it could just be a pattern. Suppose template constraints as well as static assert (heck maybe even normal assert) just looked for a method, maybe __compileErrors. If present, it processes the return value as supplemental errors.

Now the library defines

struct MyChecks {
    bool opBool(T:bool) { return errors.length == 0; }
    string[] __compileErrors;
}

MyChecks isInputRange(T)() {
     MyChecks errors;
     if(!hasMember!(T, "front")
        errors.__compileErrors ~= "missing front";
     if(!hasMember!(T, "popFront")
        errors.__compileErrors ~= "missing popFront";
     if(!hasMember!(T, "empty")
        errors.__compileErrors ~= "missing empty";

     return errors;
}



Then user code does:


static assert(isInputRange!MyThing); // automatically shows the errors if it fails (unless the user specifies something else)

void foo(T)() if(isInputRange!T) {} // shows the errors if it fails  in addition to what it has now


Destroy.
September 14
On Monday, 14 September 2020 at 00:00:15 UTC, Adam D. Ruppe wrote:
> 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.
>
> 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) {}
>

Would be less nasty if https://issues.dlang.org/show_bug.cgi?id=21247 were fixed, then you could just do:

void foo(T)() if(checkInputRange!T) {}
September 14
On Monday, 14 September 2020 at 07:52:25 UTC, Nicholas Wilson wrote:
> Would be less nasty if https://issues.dlang.org/show_bug.cgi?id=21247 were fixed, then you could just do:

Yea, if that worked together with the dip it wouldn't be too bad.
September 14
On 9/13/20 8:00 PM, Adam D. Ruppe wrote:
> 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"?

Yes, indeed the recent improvements are noticeable. On a synthetic example it looks all dandy, but clearly there's be a large improvement by allowing semantic error messages. I just happened to have std.algorithm.comparison opened and here are a few examples:

template among(values...)
if (isExpressionTuple!values, "All values in the among expression must be expressions")

auto max(T...)(T args)
if (T.length >= 2)
if (!is(CommonType!T == void), "Types "~T.stringof~" don't have a common type for choosing the maximum.")

(similar for min)

CommonType!(T, Ts) either(alias pred = a => a, T, Ts...)(T first, lazy Ts alternatives)
if (alternatives.length >= 1, "Need at least one alternative besides primary choice")
if (!is(CommonType!(T, Ts) == void), "Types "~AliasSeq!(T, Ts).stringof~" need a common type")
if (allSatisfy!(ifTestable, T, Ts), "Types "~AliasSeq!(T, Ts).stringof~" must be testable with the if (...) statement")

Better error messages are always a good investment.
September 14
On Monday, 14 September 2020 at 15:28:29 UTC, Andrei Alexandrescu wrote:
> Yes, indeed the recent improvements are noticeable. On a synthetic example it looks all dandy, but clearly there's be a large improvement by allowing semantic error messages. I just happened to have std.algorithm.comparison opened and here are a few examples:
>
> template among(values...)
> if (isExpressionTuple!values, "All values in the among expression must be expressions")
>
> auto max(T...)(T args)
> if (T.length >= 2)
> if (!is(CommonType!T == void), "Types "~T.stringof~" don't have a common type for choosing the maximum.")
>
> (similar for min)
>
> CommonType!(T, Ts) either(alias pred = a => a, T, Ts...)(T first, lazy Ts alternatives)
> if (alternatives.length >= 1, "Need at least one alternative besides primary choice")
> if (!is(CommonType!(T, Ts) == void), "Types "~AliasSeq!(T, Ts).stringof~" need a common type")
> if (allSatisfy!(ifTestable, T, Ts), "Types "~AliasSeq!(T, Ts).stringof~" must be testable with the if (...) statement")
>
> Better error messages are always a good investment.

Don't like that syntax at all. If statements aren't assert statements. No where else is an if statement used in that manor, worse so in that that would have actually been valid were the comma expression still accepted. So it is the same syntax with different meaning.

For your example with "max", the reason there isn't a good error message for that, depending on what you pass to it. Is that "MaxType" doesn't check if the type passed in has a ".max" variable/component. So that's where it errors and it isn't handled properly. The template could be changed to ensure that the type being passed in is numerical, which it currently doesn't but the way it functions seems to assume that it is. Ultimately how code is written is going to affect the readability more so than adding more features.
September 15
On Monday, 14 September 2020 at 12:42:31 UTC, Adam D. Ruppe wrote:
> On Monday, 14 September 2020 at 07:52:25 UTC, Nicholas Wilson wrote:
>> Would be less nasty if https://issues.dlang.org/show_bug.cgi?id=21247 were fixed, then you could just do:
>
> Yea, if that worked together with the dip it wouldn't be too bad.

First crack: https://github.com/dlang/dmd/pull/11733/files
September 17
On Monday, September 14, 2020 9:28:29 AM MDT Andrei Alexandrescu via Digitalmars-d wrote:
> Yes, indeed the recent improvements are noticeable. On a synthetic example it looks all dandy, but clearly there's be a large improvement by allowing semantic error messages. I just happened to have std.algorithm.comparison opened and here are a few examples:
>
> template among(values...)
> if (isExpressionTuple!values, "All values in the among expression must
> be expressions")
>
> auto max(T...)(T args)
> if (T.length >= 2)
> if (!is(CommonType!T == void), "Types "~T.stringof~" don't have a common
> type for choosing the maximum.")
>
> (similar for min)
>
> CommonType!(T, Ts) either(alias pred = a => a, T, Ts...)(T first, lazy
> Ts alternatives)
> if (alternatives.length >= 1, "Need at least one alternative besides
> primary choice")
> if (!is(CommonType!(T, Ts) == void), "Types "~AliasSeq!(T,
> Ts).stringof~" need a common type")
> if (allSatisfy!(ifTestable, T, Ts), "Types "~AliasSeq!(T, Ts).stringof~"
> must be testable with the if (...) statement")
>
> Better error messages are always a good investment.

Honestly, IMHO, that makes the code far worse. You're just repeating the constraints in plain English, which requires more than double the code for each constraint and doesn't really make the error messages much easier to understand IMHO - especially when the constraints are already using traits that make what they're doing clear.

Having the compiler indicate which parts of a constraint are true or false could be useful, and I'm sure that we could improve the error messages, but I think that there's a problem if we're having to put a bunch of English text into the constraints to explain what's going on - especially when it's generally just going to be saying the same thing as the code except in English. It's like commenting every line to say what it does.

And as Adam pointed out, the problem is usually in figuring out why a particular constraint is failing, not what a constraint means, and that has a tendency to result you having to do stuff like copy the constraint into your code and breaking it apart to test each piece and then potentially digging into the traits that it's using (such as isInputRange) to figure out why that particular trait is true or false. I have a lot of trouble believing that translating stuff into English text is going to actually help in practice. In some cases, it might help someone who really isn't familiar with how traits and template constraints work, but for someone familiar with D code, I would expect it to just be redundant and that it wouldn't help with the actual problem. It also has the problem that comments can have in that if you're not careful, the code and message may not match up later on as the code is maintained.

I really think that we should be looking at how we can get the compiler to give us better information and reduce how much we have to do stuff like copy the constraint and test out its parts separately from the template if we want to make dealing with template constraints easier.

- Jonathan M Davis



September 17
On 9/17/20 3:16 PM, Jonathan M Davis wrote:
> Honestly, IMHO, that makes the code far worse. You're just repeating the
> constraints in plain English, which requires more than double the code for
> each constraint and doesn't really make the error messages much easier to
> understand IMHO - especially when the constraints are already using traits
> that make what they're doing clear.

This has got to be the ultimate in reverse psychology. Picture this: I set out to save Walter some work. I took what seemed a motherhood and apple pie argument - I mean, who in the world would argue against better error messages? Yes, in Queen's English! It turns out, those can be found.

Now imagine I wrote this instead:

How about we do some DIP cleanup? All those dead and moribund DIPs laying around. I suggest we start with https://github.com/dlang/DIPs/pull/131 - it really does nothing else but repeat the constraints in natural language. There's also been improvements in how the compiler displays errors. I propose we just chop that DIP.
« First   ‹ Prev
1 2 3 4