digitalmars.D - Now that's a DIP that could use some love
- Andrei Alexandrescu (15/15) Sep 13 2020 https://github.com/dlang/DIPs/pull/131
- Adam D. Ruppe (110/112) Sep 13 2020 Since this was written, the compiler's output has improved
- Adam D. Ruppe (30/33) Sep 13 2020 Here's a slight refinement: it does the cast to bool already. So
- Nicholas Wilson (5/13) Sep 14 2020 Would be less nasty if
- Adam D. Ruppe (3/6) Sep 14 2020 Yea, if that worked together with the dip it wouldn't be too bad.
- Nicholas Wilson (2/9) Sep 14 2020 First crack: https://github.com/dlang/dmd/pull/11733/files
- Andrei Alexandrescu (22/51) Sep 14 2020 Yes, indeed the recent improvements are noticeable. On a synthetic
- Jackel (16/38) Sep 14 2020 Don't like that syntax at all. If statements aren't assert
- Jonathan M Davis (31/52) Sep 17 2020 Honestly, IMHO, that makes the code far worse. You're just repeating the
- Andrei Alexandrescu (12/17) Sep 17 2020 This has got to be the ultimate in reverse psychology. Picture this: I
- Avrina (11/17) Sep 17 2020 That's a strawman. If all you actually were doing was making
- Jonathan M Davis (18/35) Sep 17 2020 I really don't think that repeating a constraint in English improves the
- H. S. Teoh (32/45) Sep 17 2020 [...]
- Andrei Alexandrescu (16/27) Sep 17 2020 Not at all! What in the world...? The constraint is the mechanics, it
- Jonathan M Davis (15/45) Sep 17 2020 The problems with each are that it supports too many things and that it ...
- Andrei Alexandrescu (19/23) Sep 17 2020 Here goes, straight from the horse's mouth
- jmh530 (9/10) Sep 17 2020 I don't think the people who disagree and instead think that the
- jmh530 (35/45) Sep 17 2020 I also want to draw some attention to another possible solution
- Jonathan M Davis (94/121) Sep 17 2020 The constraints themselves are short and look pretty clear to me:
- H. S. Teoh (42/62) Sep 17 2020 Yes, and *this* is the crux of the issue here. Get this right, and the
- Timon Gehr (23/29) Sep 18 2020 So `each` says it works for types that cannot be iterated with `foreach`...
- Andrei Alexandrescu (9/43) Sep 19 2020 Thanks. It's interesting that those who opined the constraints are
- Adam D. Ruppe (9/13) Sep 19 2020 I'm not necessarily against providing a string - indeed, in my
- Andrei Alexandrescu (3/4) Sep 19 2020 Huh. I was sure D includes the failing expression a la C. Apparently I
- Seb (4/9) Sep 19 2020 Huh, you are aware of -checkaction-context right?
- Adam D. Ruppe (6/8) Sep 19 2020 Of course, that's why I said "without extra steps".
- H. S. Teoh (75/92) Sep 17 2020 A sig constraint that contains the equivalent of an LR step does not
- Andrei Alexandrescu (25/28) Sep 17 2020 Emphatically NO!
- Nick Treleaven (7/12) Sep 18 2020 In general it's not practical to always (or even usually) write
- Paul Backus (5/15) Sep 17 2020 +1
- Meta (25/32) Sep 18 2020 If we're willing to rewrite most of the template constraints in
- Meta (5/5) Sep 18 2020 On Friday, 18 September 2020 at 15:43:44 UTC, Meta wrote:
- Alexandru Ermicioi (8/26) Sep 18 2020 Why not return a struct that has result of test and message for
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
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
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
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: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) {}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) {}
Sep 14 2020
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
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:First crack: https://github.com/dlang/dmd/pull/11733/filesWould 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
On 9/13/20 8:00 PM, Adam D. Ruppe wrote:On Sunday, 13 September 2020 at 22:29:00 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.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"?
Sep 14 2020
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
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
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
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
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: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 DavisHonestly, 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
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
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: [...]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.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.
Sep 17 2020
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: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 DavisOn Thu, Sep 17, 2020 at 01:16:13PM -0600, Jonathan M Davis via Digitalmars-d wrote: [...]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.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.
Sep 17 2020
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
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
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: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 }[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
On Thursday, 17 September 2020 at 23:10:23 UTC, jmh530 wrote: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)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 18 2020
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
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:The constraints themselves are short and look pretty clear to me: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" ............. FAILif (!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
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
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
On 9/18/20 4:13 AM, Timon Gehr wrote:On 17.09.20 23:55, Andrei Alexandrescu wrote: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.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 19 2020
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
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
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
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
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:[...]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.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)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
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
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
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
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
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
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