www.digitalmars.com         C & C++   DMDScript  

digitalmars.D - Now that's a DIP that could use some love

reply Andrei Alexandrescu <SeeWebsiteForEmail erdani.org> writes:
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).
Sep 13 2020
next sibling parent reply Adam D. Ruppe <destructionator gmail.com> writes:
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.
Sep 13 2020
next sibling parent Adam D. Ruppe <destructionator gmail.com> writes:
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.
Sep 13 2020
prev sibling next sibling parent reply Nicholas Wilson <iamthewilsonator hotmail.com> writes:
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) {}
Sep 14 2020
parent reply Adam D. Ruppe <destructionator gmail.com> writes:
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.
Sep 14 2020
parent Nicholas Wilson <iamthewilsonator hotmail.com> writes:
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
Sep 14 2020
prev sibling parent reply Andrei Alexandrescu <SeeWebsiteForEmail erdani.org> writes:
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.
Sep 14 2020
next sibling parent Jackel <jackel894_394 gmail.com> writes:
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.
Sep 14 2020
prev sibling next sibling parent reply Jonathan M Davis <newsgroup.d jmdavisprog.com> writes:
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
Sep 17 2020
parent reply Andrei Alexandrescu <SeeWebsiteForEmail erdani.com> writes:
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.
Sep 17 2020
next sibling parent Avrina <avrina12309412342 gmail.com> writes:
On Thursday, 17 September 2020 at 19:34:49 UTC, Andrei 
Alexandrescu wrote:
 On 9/17/20 3:16 PM, Jonathan M Davis wrote:
 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.
That's a strawman. If all you actually were doing was making better error messages, then no one would argue against it. What you are proposing is adding a new feature, with new syntax that runs counter to existing facilities of the language. Introducing complexity and debt, while ultimately still relying on the user to know what they are doing to produce those better error messages. Which, if someone knows what they are doing is able to accomplish this now to the same degree as the proposed new feature.
Sep 17 2020
prev sibling parent Jonathan M Davis <newsgroup.d jmdavisprog.com> writes:
On Thursday, September 17, 2020 1:34:49 PM MDT Andrei Alexandrescu via 
Digitalmars-d wrote:
 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.
I really don't think that repeating a constraint in English improves the error messages particularly, and as shown in your example, it gets pretty verbose. In practice, I've never seen reading the constraint as the problem when dealing with a failed template constraint. The constraint itself already explains itself in code. The problem is figuring out which part is failing and why, and that usually requires digging deeper. IMHO, _that_ is the problem that would be useful to solve if we want to make it easier to solve problems with failed template constraints. Simply repeating the constraint in English doesn't do that. The only part of that DIP that looks useful to me is the fact that it breaks up the constraint so that you can see which part failed, but simply having the compiler break out the constraint along each && clause and print whether it was true or false would do the same thing without making a lot of template constraints more verbose. I'd be fine with the DIP being chopped. IMHO, it doesn't realy help with the real problem. - Jonathan M Davis
Sep 17 2020
prev sibling parent reply "H. S. Teoh" <hsteoh quickfur.ath.cx> writes:
On Thu, Sep 17, 2020 at 01:16:13PM -0600, Jonathan M Davis via Digitalmars-d
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.
[...]
 It's like commenting every line to say what it does.
Exactly, this breaks DRY.
 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.
[...] That's totally been my experience with writing heavy range-based UFCS code. These days, thanks to recent efforts to improve error messages, the compiler will now come back with "no matching overload: must satisfy constraints: X, Y, Z". Which is a good start; however, it doesn't go far enough, because these days Phobos sig constraints are opaque: auto somePhobosFunc(R)(R r) if (isInputRange!R && satisfiesX!R && satisfiesY!R && ...) The compiler tells me my argument fails the constraint `satisfiesX!R`, but `satisfiesX` is an internal Phobos helper template; I have no idea what it's checking for and why my argument fails to satisfy it. At the end of the day, I have to go digging into Phobos code, copy-n-paste it into my own, then eliminate the failing conditions one by one, just like Jonathan says. Instead of this half-baked hack of essentially writing comments that repeat what the code already says, what we should be doing is to take the current error messages one step further: when some clause in a sig constraint fails, how about the compiler ungags the errors that cropped up while evaluating that clause? Instead of blindly ungagging *everything* (*cough*-verrors=spec*cough*), which results in a deluge of irrelevant errors that the one relevant message gets lost in, ungag only the most likely relevant errors: the ones produced when evaluating a failed sig constraint clause. I think that would improve error messages much more than the repeat-after-yourself exercise proposed here. T -- Obviously, some things aren't very obvious.
Sep 17 2020
next sibling parent reply Andrei Alexandrescu <SeeWebsiteForEmail erdani.com> writes:
On 9/17/20 3:58 PM, H. S. Teoh wrote:
 On Thu, Sep 17, 2020 at 01:16:13PM -0600, Jonathan M Davis via Digitalmars-d
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.
[...]
 It's like commenting every line to say what it does.
Exactly, this breaks DRY.
Not at all! What in the world...? The constraint is the mechanics, it often says little about the high-level requirements. Granted, sometimes the mechanism is simple enough to be sufficiently evocative. But would you really like the compiler error messages in terms of the LR step that failed? It's a funny coincidence this thread is going in parallel with a couple of other developments: * `each` has literally inscrutable constraints (I say "literally" because their documentation is not visible) * `equals` also has nigh unreadable constraints, see https://github.com/dlang/phobos/pull/7635/files If you (cut and) DRY that stuff, I'll eat it. I found these in literally the first two files I looked at in Phobos. I can't believe I need to argue this stuff. DRY? No, it's wet like a drowned rat.
Sep 17 2020
next sibling parent reply Jonathan M Davis <newsgroup.d jmdavisprog.com> writes:
On Thursday, September 17, 2020 2:28:21 PM MDT Andrei Alexandrescu via 
Digitalmars-d wrote:
 On 9/17/20 3:58 PM, H. S. Teoh wrote:
 On Thu, Sep 17, 2020 at 01:16:13PM -0600, Jonathan M Davis via
 Digitalmars-d 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.
[...]
 It's like commenting every line to say what it does.
Exactly, this breaks DRY.
Not at all! What in the world...? The constraint is the mechanics, it often says little about the high-level requirements. Granted, sometimes the mechanism is simple enough to be sufficiently evocative. But would you really like the compiler error messages in terms of the LR step that failed? It's a funny coincidence this thread is going in parallel with a couple of other developments: * `each` has literally inscrutable constraints (I say "literally" because their documentation is not visible) * `equals` also has nigh unreadable constraints, see https://github.com/dlang/phobos/pull/7635/files If you (cut and) DRY that stuff, I'll eat it. I found these in literally the first two files I looked at in Phobos. I can't believe I need to argue this stuff. DRY? No, it's wet like a drowned rat.
The problems with each are that it supports too many things and that it uses private traits in the public constraints. But even then, the actual template constraints are pretty short, and the traits used are descriptive enough that I wouldn't expect them to be hard to understand or that repeating them in English text would help much. Their names are basically just a truncated version of what I'd expect the English text to say anyway. Where things really get ugly is all of the static if conditions, but those are internal and shouldn't affect the user. Either way, if each really just supported ranges and required that wrappers or explicit conversions be used for anything else, then the private traits wouldn't be required in the constraint, and the constraint would be straightforward. - Jonathan M Davis
Sep 17 2020
parent reply Andrei Alexandrescu <SeeWebsiteForEmail erdani.org> writes:
On 9/17/20 5:39 PM, Jonathan M Davis wrote:
 private traits in the public constraints. But even then, the actual template
 constraints are pretty short, and the traits used are descriptive enough
 that I wouldn't expect them to be hard to understand or that repeating them
 in English text would help much.
Here goes, straight from the horse's mouth (https://dlang.org/library/std/algorithm/iteration/each.each.html): Flag!"each" each(Range) ( Range r ) if (!isForeachIterable!Range && (isRangeIterable!Range || __traits(compiles, typeof(r.front).length))); Flag!"each" each(Iterable) ( auto ref Iterable r ) if (isForeachIterable!Iterable || __traits(compiles, Parameters!(Parameters!(r.opApply)))); Compare to "`each` requires type MyRange to work with foreach". Let's see: "pretty short" .................................................. FAIL "descriptive enough" ............................................ FAIL "wouldn't expect them to be hard to understand" ................ FAIL "repeating them in English text wouldn't help much" ............. FAIL
Sep 17 2020
next sibling parent reply jmh530 <john.michael.hall gmail.com> writes:
On Thursday, 17 September 2020 at 21:55:22 UTC, Andrei 
Alexandrescu wrote:
 [snip]
I don't think the people who disagree and instead think that the situation is good. HS Teoh recommended an alternative approach that may provide simpler error reports. His approach would effectively widen the template constraints to something like just isInputRange. One could still make an argument about isInputRange not provided valuable error reporting, but that is one thing that Atila's concepts library attempts to fix.
Sep 17 2020
parent reply jmh530 <john.michael.hall gmail.com> writes:
On Thursday, 17 September 2020 at 22:29:50 UTC, jmh530 wrote:
 On Thursday, 17 September 2020 at 21:55:22 UTC, Andrei 
 Alexandrescu wrote:
 [snip]
I don't think the people who disagree and instead think that the situation is good. HS Teoh recommended an alternative approach that may provide simpler error reports. His approach would effectively widen the template constraints to something like just isInputRange. One could still make an argument about isInputRange not provided valuable error reporting, but that is one thing that Atila's concepts library attempts to fix.
I also want to draw some attention to another possible solution to this morass, which is enhancing template alias deduction. Someone was working on a DIP for this, but their approach became tricky in the more general case. Ideally something like below could work where the call to foo(y) would produce the result from the static assert. In other words, instantiating foo with T should cause ArrayType(T) to be instantiated, which could either produce the relevant alias that would be used for deduction or the result of the static assert appears before any additional matching would go on. A similar template alias for InputRange (instead of isInputRange) would basically return the type if it passes the relevant checks and a detailed error message otherwise. I would think this would somehow fit in with Stefan Koch's work on type/alias functions... template ArrayType(T) { import std.traits: isDynamicArray; static if (isDynamicArray!T) { alias ArrayType = T; } else { static assert(0, "T is not a valid ArrayType"); } } void foo(T)(ArrayType!T x) { } void main() { int[] x; foo(x); //does not currently compile int y; foo(y); //does not currently compile, shouldn't compile }
Sep 17 2020
parent reply claptrap <clap trap.com> writes:
On Thursday, 17 September 2020 at 23:10:23 UTC, jmh530 wrote:

 template ArrayType(T)
 {
     import std.traits: isDynamicArray;
     static if (isDynamicArray!T) {
         alias ArrayType = T;
     } else {
         static assert(0, "T is not a valid ArrayType");
     }
 }

 void foo(T)(ArrayType!T x) {
  	
 }

 void main()
 {
     int[] x;
     foo(x); //does not currently compile
     int y;
     foo(y); //does not currently compile, shouldn't compile
 }
Pretty cool that. It occurs to me that it may not be even necessary to declare the template parameter up front, it could just be... void foo(ArrayType!T x) That could be lowered / rewritten by the compiler back to.. void foo(T)(ArrayType!T x)
Sep 18 2020
parent jmh530 <john.michael.hall gmail.com> writes:
On Friday, 18 September 2020 at 09:32:29 UTC, claptrap wrote:
 [snip]

 Pretty cool that. It occurs to me that it may not be even 
 necessary to declare the template parameter up front, it could 
 just be...

 void foo(ArrayType!T x)

 That could be lowered / rewritten by the compiler back to..

 void foo(T)(ArrayType!T x)
I don't know. The compiler would need to know what T is. You might be special-casing a bit too much.
Sep 18 2020
prev sibling next sibling parent Jonathan M Davis <newsgroup.d jmdavisprog.com> writes:
On Thursday, September 17, 2020 3:55:22 PM MDT Andrei Alexandrescu via 
Digitalmars-d wrote:
 On 9/17/20 5:39 PM, Jonathan M Davis wrote:
 private traits in the public constraints. But even then, the actual
 template constraints are pretty short, and the traits used are
 descriptive enough that I wouldn't expect them to be hard to understand
 or that repeating them in English text would help much.
Here goes, straight from the horse's mouth (https://dlang.org/library/std/algorithm/iteration/each.each.html): Flag!"each" each(Range) ( Range r ) if (!isForeachIterable!Range && (isRangeIterable!Range || __traits(compiles, typeof(r.front).length))); Flag!"each" each(Iterable) ( auto ref Iterable r ) if (isForeachIterable!Iterable || __traits(compiles, Parameters!(Parameters!(r.opApply)))); Compare to "`each` requires type MyRange to work with foreach". Let's see: "pretty short" .................................................. FAIL "descriptive enough" ............................................ FAIL "wouldn't expect them to be hard to understand" ................ FAIL "repeating them in English text wouldn't help much" ............. FAIL
The constraints themselves are short and look pretty clear to me:
 if (!isForeachIterable!Range && (isRangeIterable!Range ||
 __traits(compiles, typeof(r.front).length)));
 if (isForeachIterable!Iterable || __traits(compiles,
 Parameters!(Parameters!(r.opApply))));
Now, they could definitely use some improvement (well, a lot of improvement), but I don't think that they're particularly obtuse even if they're poorly written. On the other hand, I don't see how "`each` requires type MyRange to work with foreach" would be particularly useful information. What's MyRange? It sounds like a roundabout way of saying that each requires a range, which isn't what it actually requires at all (even it arguably should be what it requires). What it really requires is a type that's iterable with foreach where the given function can be called with the element type of the iterable. The current template constraints test that the type is iterable with foreach and have a branch for ranges and one for other types that work with foreach. And having branches complicates things (also having isForeachIterable be false for ranges makes it a poor name, though it's clear enough what's meant from the context). In addition, they fail to test that the function is callable with the element type, leaving that for the static ifs inside the function overloads, which is definitely a bug, since then you can get a compilation error within the function even if the arguments pass the constraint. So, even if the constraints are understandable, they obviously need work. Given the traits that we currently have, the template constraint should probably be something like if (__traits(compiles, { foreach(e; r) { unaryFun!fun(e); } }) || __traits(compiles, { foreach(i, e; r) { binaryFun!fun(i, e); } })) and if we really need an overload because of the auto ref on the second overload, then it should probably be something like if (isInputRange!Range && __traits(compiles, unaryFun!fun(r.front))) and if (!isInputRange!Iterable && (__traits(compiles, { foreach(e; r) { unaryFun!fun(e); } }) || __traits(compiles, { foreach(i, e; r) { binaryFun!fun(i, e); } }))) In any case, regardless of what the exact template constraint is, let's say that we had a message to go along with it that said what was required in English. e.g. "`each` requires a type which is iterable with foreach and whose element type is callable with the function provided as the template argument." That doesn't really encompass the weirdness with the binary version, but it gets across the unary version clearly enough and makes it clear what you need to pass to each when the provided function is unary. However, IMHO, it still isn't very useful, because if it fails, it doesn't tell me what I'm doing wrong. I should already know the abstract requirements of each if I'm calling it, so the message is basically just telling me that my arguments are not meeting the abstract requirements that I already knew about. At best, it's helping me undersand what those abstract requirements are if I tried to use each without actually reading the documention. But regardless, it doesn't actually tell me what's wrong, just like seeing the template constraint itself doesn't actually tell me what's wrong. What I need to know is _why_ my arguments are not meeting the function template's requirements. And unless that's really obvious by just glancing at the code, because I made a simple and obvious mistake, that probably means that I'm going to have to copy the template constraint into my own code and test each piece individually to see which pieces are true and which are false. And once I know that, I may have to dig deeper and go inside the traits that were used in the template constraint and copy their contents into my code so that I can check each piece individually to see what's true and what's false so I can figured out where the problem is. An English message may help you understand a bad template constraint, but if a template constraint is so hard to understand that it needs a message in English, then it generally means that either the function itself is overly complicated, we're missing a trait for a general concept that we should have a trait for, and/or the function template is overloaded when the differences should be inside the template with static if branches instead of being in the template constraints of overloads. In general, having if (cond && stuff) and if (!cond && otherStuff) is an anti-pattern and should be avoided where possible. With a properly cleaned up template constraint, an English message would just be rewording the constraint into English. So, it wouldn't be clarifying much, and you would still need to look at the actual template constraint (and possibly the implementations of the traits that it uses) in order to know why the template constraint was failing. So, as far as I can tell, unless a template constraint is badly written, the English message would be redundant and unhelpful - and if it's shown instead of the actual template constraint, then it's actively making it harder to get at the information needed to figure out what's wrong. If the compiler is going to make life easier when a template constraint fails, then it needs to be giving information about what specifically in the constraint is true and false so that you don't have to copy it into your code and compile it to figure it out. Additionally, when it gags an error within a template constraint, it would be useful to know what it gagged so that you don't have to copy the pieces out to get that information. And all of that relates to understanding the actual template constraint, what exactly it's testing, and exactly which pieces are failing, not the abstract concept that a message in English would provide. With a well-written template constraint, the abstract concept and the template constraint itself would be as close as reasonably possible, but regardless of whether an additional message translating the constraint into English is provided, it's still the actual template constraint that you need information about in order to debug the problem. - Jonathan M Davis
Sep 17 2020
prev sibling next sibling parent "H. S. Teoh" <hsteoh quickfur.ath.cx> writes:
On Thu, Sep 17, 2020 at 08:43:02PM -0600, Jonathan M Davis via Digitalmars-d
wrote:
[...]
 What I need to know is _why_ my arguments are not meeting the function
 template's requirements. And unless that's really obvious by just
 glancing at the code, because I made a simple and obvious mistake,
 that probably means that I'm going to have to copy the template
 constraint into my own code and test each piece individually to see
 which pieces are true and which are false. And once I know that, I may
 have to dig deeper and go inside the traits that were used in the
 template constraint and copy their contents into my code so that I can
 check each piece individually to see what's true and what's false so I
 can figured out where the problem is.
Yes, and *this* is the crux of the issue here. Get this right, and the rest is just cosmetic cleanup. Get this wrong, and no matter how many cosmetics you pile on, it's still a carcass. This is why I suggested that the compiler should ungag errors for the constraint clause that failed to be met. *That* would be a lot more useful because it will actually tell me what went wrong, instead of papering over it with some generic abstract message that, as Jonathan says, I should have already known before calling the function. Quite often, when there's a typo or some kind of bug in my code, the error comes in the form of some lambda or range failing to meet some sig constraints of a Phobos function. The sig constraint may be checking that something is a forward range. I *already* know that the function is expecting a forward range; and I wrote the UFCS chain believing that the resulting of the preceding part of the chain returns a forward range, and now it comes back and says "function XYZ expects a forward range". Well no kidding, I already knew that. What I need to know is, *why* is the preceding part of the UFCS chain *not* a forward range when I expected it to be one? For this, I want to see what exactly about a forward range's requirements I failed to meet. Was it because I left out .save again? Or was it because I had a typo and .front doesn't compile, so it failed to qualify as a forward range? Currently, these failures are gagged, and the compiler substitutes in its place "argument must satisfy constraint: isForwardRange!R", which is of no help. Even if this were replaced with a hand-written message (in Queen's English, no less), it would still be of no help. Just stop dithering about, and TELL ME THE DARNED COMPILE ERROR DAMMIT!!!!! I'm a programmer; I won't get scared by messages like "syntax error in myRange.front: no such identifier 'xyz'". Or "cannot call .front from safe code because it escapes a reference to member .zzz". In fact, these are the very errors that will actually tell me what I need to fix in my code. These are the errors that I *want* to see. Substituting these "ugly" errors with some veneer of Queen's English is no different from "argument must satisfy constraint: isForwardRange!R", which essentially amounts to "hahaha your range fails to be a forward range, and ninner-ninners, I'm not gonna tell you why!". [...]
 If the compiler is going to make life easier when a template
 constraint fails, then it needs to be giving information about what
 specifically in the constraint is true and false so that you don't
 have to copy it into your code and compile it to figure it out.
 Additionally, when it gags an error within a template constraint, it
 would be useful to know what it gagged so that you don't have to copy
 the pieces out to get that information. And all of that relates to
 understanding the actual template constraint, what exactly it's
 testing, and exactly which pieces are failing, not the abstract
 concept that a message in English would provide.
Exactly. T -- Don't throw out the baby with the bathwater. Use your hands...
Sep 17 2020
prev sibling parent reply Timon Gehr <timon.gehr gmx.ch> writes:
On 17.09.20 23:55, Andrei Alexandrescu wrote:
 
 Flag!"each" each(Range) (
    Range r
 )
 if (!isForeachIterable!Range && (isRangeIterable!Range || 
 __traits(compiles, typeof(r.front).length)));
So `each` says it works for types that cannot be iterated with `foreach` but whose `front` has a `length` member? :-) Turns out it does not: ---- import std.algorithm; struct S{ struct T{ struct Q{} Q length; } T front; } void main(){ S().each!((x){}); } ---- std/algorithm/iteration.d(966): Error: template `std.range.primitives.empty` cannot deduce function from argument types `!()(S)`, candidates are: ... ---- https://issues.dlang.org/show_bug.cgi?id=21264
Sep 18 2020
parent reply Andrei Alexandrescu <SeeWebsiteForEmail erdani.org> writes:
On 9/18/20 4:13 AM, Timon Gehr wrote:
 On 17.09.20 23:55, Andrei Alexandrescu wrote:
 Flag!"each" each(Range) (
    Range r
 )
 if (!isForeachIterable!Range && (isRangeIterable!Range || 
 __traits(compiles, typeof(r.front).length)));
So `each` says it works for types that cannot be iterated with `foreach` but whose `front` has a `length` member? :-) Turns out it does not: ---- import std.algorithm; struct S{     struct T{         struct Q{}         Q length;     }     T front; } void main(){     S().each!((x){}); } ---- std/algorithm/iteration.d(966): Error: template `std.range.primitives.empty` cannot deduce function from argument types `!()(S)`, candidates are: ... ---- https://issues.dlang.org/show_bug.cgi?id=21264
Thanks. It's interesting that those who opined the constraints are pretty easy to read didn't even notice this one's wrong. If you think it's easy and got it wrong, it's not easy! Second, the same argument applies to assert. Why provide a string with the assert when all it does is it repeats the expression in human language? Yet most asserts do provide explanatory strings, and I can only assume many people find those useful. The mechanics is not the design.
Sep 19 2020
parent reply Adam D. Ruppe <destructionator gmail.com> writes:
On Saturday, 19 September 2020 at 14:44:18 UTC, Andrei 
Alexandrescu wrote:
 Why provide a string with the assert when all it does is it 
 repeats the expression in human language?
I'm not necessarily against providing a string - indeed, in my proposal, you would provide strings, plural - but the questions of what they are, how they got there, and when they are specified are inadequately answered by this DIP.
 Yet most asserts do provide explanatory strings, and I can only 
 assume many people find those useful.
this is apples and oranges anyway because with asserts you can't see the original source and values that failed without extra steps. "foo.d:3 Assertion failure" isn't exactly great.
Sep 19 2020
next sibling parent Andrei Alexandrescu <SeeWebsiteForEmail erdani.org> writes:
On 9/19/20 10:55 AM, Adam D. Ruppe wrote:
 "foo.d:3 Assertion failure" isn't exactly great.
Huh. I was sure D includes the failing expression a la C. Apparently I haven't seen a bald assert in a long time.
Sep 19 2020
prev sibling parent reply Seb <seb wilzba.ch> writes:
On Saturday, 19 September 2020 at 14:55:09 UTC, Adam D. Ruppe 
wrote:
 On Saturday, 19 September 2020 at 14:44:18 UTC, Andrei 
 Alexandrescu wrote:

 this is apples and oranges anyway because with asserts you 
 can't see the original source and values that failed without 
 extra steps. "foo.d:3 Assertion failure" isn't exactly great.
Huh, you are aware of -checkaction-context right? We should just make it the default for all non-release builds.
Sep 19 2020
parent Adam D. Ruppe <destructionator gmail.com> writes:
On Saturday, 19 September 2020 at 15:17:50 UTC, Seb wrote:
 Huh, you are aware of -checkaction-context right?
Of course, that's why I said "without extra steps". If it was the default, I suspect the amount of strings we see would decrease.
 We should just make it the default for all non-release builds.
yeah, the message is a lot nicer since it even prints out the values!
Sep 19 2020
prev sibling parent reply "H. S. Teoh" <hsteoh quickfur.ath.cx> writes:
On Thu, Sep 17, 2020 at 04:28:21PM -0400, Andrei Alexandrescu via Digitalmars-d
wrote:
 On 9/17/20 3:58 PM, H. S. Teoh wrote:
[...]
 Exactly, this breaks DRY.
Not at all! What in the world...? The constraint is the mechanics, it often says little about the high-level requirements. Granted, sometimes the mechanism is simple enough to be sufficiently evocative. But would you really like the compiler error messages in terms of the LR step that failed?
A sig constraint that contains the equivalent of an LR step does not belong in the constraint, it belongs in a static if inside the function body. Or at the very least, it belongs in a separately-documented template with a descriptive name that indicates what exactly it's testing for. The sig constraint is meant for the outside world to read and digest. Why do you think we write: auto myRangeFunc(R)(R r) if (isInputRange!R) { ... } instead of: auto myRangeFunc(R)(R r) if (is(typeof(R.init) == R) && is(ReturnType!((R r) => r.empty) == bool) && is(typeof((return ref R r) => r.front)) && !is(ReturnType!((R r) => r.front) == void) && is(typeof((R r) => r.popFront))) { ... } ? Precisely because the latter is the mechanics, the former is the high-level requirements. If your sig constraints look like the latter such that you need a string description for it, then perhaps you should be thinking about refactoring it into a helper template with a descriptive name instead.
 It's a funny coincidence this thread is going in parallel with a
 couple of other developments:
 
 * `each` has literally inscrutable constraints (I say "literally"
 because their documentation is not visible)
So the problem is (1) the constraints ought to be publicly documented, and (2) if that doesn't make sense, then .each ought to be rewritten with a different charter, as you said yourself. How exactly does adding a string description to .each's sig constraints help in any way? Wouldn't they just repeat what the names of the clauses already say? (And if the names are not descriptive enough, that's all the more reason to write a fuller description in the docs for the helper templates, which in either case should be made public.)
 * `equals` also has nigh unreadable constraints, see
 https://github.com/dlang/phobos/pull/7635/files
 
 If you (cut and) DRY that stuff, I'll eat it.
This is a typical ailment that plagues Phobos code: the sig constraints expose implementation details that ought to be handled by static ifs inside a common function body, instead of a bunch of needless overloads with sig constraints that are completely irrelevant to the user. As far as the user is concerned, all that matters to be able to use .equal is: 1) It takes two ranges. 2) The predicate is a binary function that compares the elements of the respective ranges. These are the only two clauses that ought to appear in the sig constraints. Everything else belongs in static ifs inside the function body (with a suitable else clause at the end that static asserts false, with a message explaining why the argument types could not be supported). All that stuff about infinite ranges and forward ranges and whatever else is in there, is implementation details that are completely irrelevant to the user. Why should the user care whether Phobos implements .equal as 5 different blocks of code, respectively taking input, forward, infinite, transfinite ranges, vs. one block of code that can handle any range? That's the Phobos maintainers' problem, it's implementation details that users do not care, and do not want to care, about. Such things do not belong in the sig constraints, but inside the function body together with the rest of the implementation details. Doing it this way will: 1) Make the sig constraints self-evident so that you don't need to explain it in excruciating detail, esp. not write a string that essentially paraphrases what the sig constraint already says; 2) Eliminate useless overloads that ought not to be part of the user-facing API in the first place -- this will reduce the size of template error messages by reducing the number of overloads (and their associated text-dumps) printed by the compiler, and make the code easier to maintain (have you ever tried to debug std.conv.to? Good luck finding which of the 15 or so overloads is the one you're looking for). 3) Replace generic useless "could not match template" errors with a static assert message that explains exactly why the given arguments cannot work. HERE is where your string message should be, not in the sig constraints. So here it is. All cut and dried for you. Now will you eat it? ;-) T -- You have to expect the unexpected. -- RL
Sep 17 2020
next sibling parent Andrei Alexandrescu <SeeWebsiteForEmail erdani.org> writes:
On 9/17/20 5:40 PM, H. S. Teoh wrote:
 How exactly does adding
 a string description to .each's sig constraints help in any way?
 Wouldn't they just repeat what the names of the clauses already say?
Emphatically NO! How is this: mymodule.d(4): Error: template onlineapp.main.each!((x) => writeln(x)).each cannot deduce function from argument types !()(int), candidates are: /dlang/dmd/linux/bin64/../../src/phobos/std/algorithm/iteration.d(962): each(Range)(Range r) with Range = int must satisfy one of the following constraints: isRangeIterable!Range __traits(compiles, typeof(r.front).length) /dlang/dmd/linux/bin64/../../src/phobos/std/algorithm/iteration.d(1023): each(Iterable)(auto ref Iterable r) with Iterable = int must satisfy one of the following constraints: isForeachIterable!Iterable __traits(compiles, Parameters!(Parameters!(r.opApply))) the same as this: mymodule.d(42): "Attempting to use `each` with type `int`, which cannot be iterated using a foreach loop." ? I mean how can anyone not chuckle at this crap? Did you guys gang on me with a prank? https://run.dlang.io/is/cbO30I
Sep 17 2020
prev sibling parent Nick Treleaven <nick geany.org> writes:
On Thursday, 17 September 2020 at 21:40:46 UTC, H. S. Teoh wrote:
 Precisely because the latter is the mechanics, the former is 
 the high-level requirements.  If your sig constraints look like 
 the latter such that you need a string description for it, then 
 perhaps you should be thinking about refactoring it into a 
 helper template with a descriptive name instead.
In general it's not practical to always (or even usually) write helper template traits, there can be a combinatorial explosion. Reading the name of such templates can be inferior to the logic of what they actually do. Helper templates should only be written if they'll be reused several times, and they need to have a clear, fairly simple concept.
Sep 18 2020
prev sibling parent Paul Backus <snarwin gmail.com> writes:
On Thursday, 17 September 2020 at 19:58:45 UTC, H. S. Teoh wrote:
 Instead of this half-baked hack of essentially writing comments 
 that repeat what the code already says, what we should be doing 
 is to take the current error messages one step further: when 
 some clause in a sig constraint fails, how about the compiler 
 ungags the errors that cropped up while evaluating that clause?
  Instead of blindly ungagging *everything* 
 (*cough*-verrors=spec*cough*), which results in a deluge of 
 irrelevant errors that the one relevant message gets lost in, 
 ungag only the most likely relevant errors: the ones produced 
 when evaluating a failed sig constraint clause.
+1 The compiler is literally *already doing* all of the necessary work to determine why a given constraint failed. We just need it to *show* us that information in a way that's actually usable.
Sep 17 2020
prev sibling parent reply Meta <jared771 gmail.com> writes:
On Sunday, 13 September 2020 at 22:29:00 UTC, Andrei Alexandrescu 
wrote:
 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.
If we're willing to rewrite most of the template constraints in Phobos, this might be workable: enum string failConstraint(string name, string msg, Args...) = `alias %s = AliasSeq!(false, %s)`.format(Args[0], processArgs!(Args[1..$])); //processArgs is a helper template that converts types to string, etc... template isInputRange(R) { static if (!is(typeof(R.init) RInit == R)) mixin(failConstraint!(isInputRange, `%s.init must have type %s, but it has type %s`, R, R, RInit); else static if (!is(ReturnType!((R r) => r.empty) REmpty == bool)) mixin(failConstraint!(isInputRange, `ReturnType!((%s r) => r.empty) must be of type bool, but it is %s`, R, REmpty); else static if (...) ...etc. else enum isInputRange = true; } As long as constraints are able to be written in this form and the proposed `if (expr, msg)` constraint syntax allows tuples to be expanded over it, just like with functions.
Sep 18 2020
next sibling parent Meta <jared771 gmail.com> writes:
On Friday, 18 September 2020 at 15:43:44 UTC, Meta wrote:
<snip>

That looks almost impossible to decipher through the newsgroup 
interface... I wish we had some sort of "formatted for code" 
equivalent like with Github et al.
Sep 18 2020
prev sibling parent Alexandru Ermicioi <alexandru.ermicioi gmail.com> writes:
On Friday, 18 September 2020 at 15:43:44 UTC, Meta wrote:
 enum string failConstraint(string name, string msg, Args...) = 
 `alias %s = AliasSeq!(false, %s)`.format(Args[0], 
 processArgs!(Args[1..$])); //processArgs is a helper template 
 that converts types to string, etc...

 template isInputRange(R)
 {
     static if (!is(typeof(R.init) RInit == R))
         mixin(failConstraint!(isInputRange, `%s.init must have 
 type %s, but it has type %s`, R, R, RInit);
     else static if (!is(ReturnType!((R r) => r.empty) REmpty == 
 bool))
         mixin(failConstraint!(isInputRange, `ReturnType!((%s r) 
 => r.empty) must be of type bool, but it is %s`, R, REmpty);
     else static if (...)
         ...etc.
     else
         enum isInputRange = true;
 }
Why not return a struct that has result of test and message for it (was mentioned before, in original discussion about multiple if constraints)? It can also be improved to aggregate multiple error messages from different tests inside a template like isInputRange!T. Regards, Alexandru.
Sep 18 2020