-
Notifications
You must be signed in to change notification settings - Fork 201
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
implement an 'implies' logical operator #4090
Comments
The linked discussion gives good answers for why implication isn't usually in programming languages. Two of the reasons is that the operator is hard to reason about, and that it's less often what you need. It's hard to reason about because it doesn't read like a test, but more like a general open predicate. I can read What is Since the alternative I expect user defined short-circuit operators to happen before a single implication operator (and I don't think we've ever thought about user defined short-circuit operators before). |
@martin-east There is no need to add a new operator, since you can just do add extension on extension ImpliesBool on bool {
bool operator >>(bool right) => !this || right;
}
void main() {
// all cases
print(true >> true); // true
print(true >> false); // false
print(false >> true); // true
print(false >> false); // true
print(true >> 150); // comptime error - you can't use implication on non-bool type
print(150 >> false); // comptime error
} IMPORTANT - DO NOT use implication when you are collaborating with someone, since implication is very difficult for people to wrap their head around. I literally had to look up the truth table for implication in order to make this extension |
An A language feature like "lazy arguments" could change that. Say a feature like: extension ImpliesBool on bool {
bool operator >>(late bool right) => !this || right;
} where I don't think such a feature is very likely, but it's also not impossible. I think it's more likely than adding an impliction short-circuit operator by itself. |
first of all I would like to thank both of you for your very prompt, thoughtful and helpful responses. Lasse's answers in particular don't just address the immediate question but give a glimpse into what might come in the future. Respect! Most of content of the link comes down to the following 'anti' arguments:
Lasse has already pointed out the problem of (lack of) short-circuiting in Peter's otherwise very helpful suggestion. I am glad if I have triggered some thoughts through my post, and personally would be very pleased to see user-defined short-circuit operators and/or lazy arguments appear in Dart. Some ideas of mine:
Many thanks again for your input Martin |
I'd argue the opposite. Those operators are in the programming languages because they are the ones that are intuitive to most people, including the people making the first programming languages. They correspond to traditional natural language phrases "this and also that" and "either this or else that". The biggest difference from natural language is that The About short-circuit bitwise operators on integers: I'm fairly sure the compiler doesn't do this. In the vast majority of cases, doing a conditional branch is more expensive than just evaluating the second expression and doing a bitwise and/or (one machine op). Unless there is reason to believe that the first operand will be zero often, not just the base 1/2^64 of the time, doing a test and still not skipping the second expression is more work. (Arguably a predictable branch then.) Dart does provide short-circuit and non-short-circuit operations for booleans ( It's non-trivial to see if an expression has side-effects, like throwing or updating global state. Allowing the compiler to choose between short-circuit and not, is a fragile approach. It's better to have the author choose, because they know what they want. Same for evaluation order. If there is one thing I want from my compiler, it's predictability. Allowing the compiler to choose evaluation can mean that tests are run in one order and production in another. That makes testing fail its one job: Avoid g bugs in production by exercising the same code and control flows in a controlled environment. If it's not testing the same control flows, it's not actually saying whether the code will run in production. (I'm not opposed to short-circuit user operators, or call-by-name parameters, but they're not trivial, and it's also not clear that they're worth the significant complexity they may entail. Not expecting anything any time soon.) |
inclusive vs. exclusive or was exactly what I was thinking of when I said that && and || aren't intuitive to non-programmers. Also, many non-programmers think that and denotes addition, thus:
I reckon that 0 and -1 arise far more frequently than any other numbers in bitwise operator arguments. My idea was that the programmer can decide which version (strict or short-circuit) is likely to give the better or safer performance, without having to completely rewrite the code. And might it make sense for the compiler to omit the test of expr1 and the evaluation of expr2 if the compiler knows expr1 is always 0 or -1 (e.g. for a named constant or where the programmer has consciously included the call to keep source code formatting orthogonal and consistent)? This is what I meant when asking if the compiler does this already.
Yes, you are correct. Since (I think) only && and || short-circuit, both options are covered. My idea was for any future short-circuit operators to always offer a strict alternative. As a side point, accidentally omitting one of the two & or | symbols can lead to some very subtle and hard-to-discover bugs. We can't change these operator symbols, but may I suggest a linter warning when using the strict versions? Short-circuit operators are always safer except for the (frankly) bizarre case that expr2 has desired side-effects that must be executed regardless of expr1, for which there really should be other linter warnings! In vscode, I get this popup when rolling over the & operator:
If I roll over a && operator, a tiny window saying only 'type bool' pops up. The same applies with | and ||. And https://dart.dev/language/operators doesn't even mention that the logical operators short-circuit! That's pretty fundamental.
That's why I proposed that the compiler chooses "... unless overridden by the programmer". Perhaps better: the author must explicitly say 'you choose' to the compiler.
That's why I proposed that the programmer actively says to the compiler: you choose. Standard C leaves the order of evaluation to the compiler in all cases: that's forced non-determinism, and I'm very glad that Dart has the opposite. It means that different Dart compilers in different environments should produce exactly the same result. I'm just suggesting that there may be cases where the programmer, by explicitly allowing the compiler to decide, could produce better performing code. Many thanks again for your time and thoughts. I'm happy to continue discussing these and related points, but fully understand if you want to focus on much more important things on your plate. NB: It would be nice to see the linter and documentation points regarding && and || addressed! Best |
I have returned, and i am sorry for misunderstanding what you wanted. You are right, the proposed solution does not short-curcuit. With that in mind:
IMO lazy arguments is a really bad proposal. What i think lazy arguments are gonna be used for is to avoid computing complex functions when possible, right? The problem is that the said function can also reference global variable, or perhaps you pass them mutable bool mutable = false;
bool calculation() => mutable;
void lazyEval(late bool value){
mutable = true;
print(value ? "IT'S FALSE" : "IT'S TRUE"); // should be false, prints true;
}
/*
// debug version
void lazyEval(late bool value){
print(value);
mutable = true;
print(value ? "IT'S FALSE" : "IT'S TRUE"); // prints false; -> which is correct
}
*/
void main(){
lazyEval(calculation());
} These kinds of bugs would be practically impossible to debug, since you think you know what is passed to function, you can also print the value at the start, and the problem would go away, since it was computed before the change happened. If you then removed the print, the bug would reappear, leaving everybody confused why is print the solution. The same would also happen with real debuggers, since when you would put the breakpoint, the value would be calculated before the change would happend, thus fixing the bug (which is like ????). The lazy evaluation of expression is a sort of async code, while looking exactly like sync code. About the userdefined short-curcuit operators, i'm not entirely sure i'm sold on it. There are not many uses for them, and most of the time, there isn't a lot of time lost. The most useful shortcurtuits have already been done, and for the others developer can implement them himself without much difficulty. For example, the collection types have functions that shortcurtuit. I just don't see a compelling enough of a reason to add a userdefined shortcurcuit operators. Also, how would you even write that?? I can't imagine any single syntax, that i'd be comfortable to read. Please provide some prototype syntax, so that we can have at least a concrete example to fall back on. |
Hi Peter, lazy evaluation is exactly what the logical operators && and || do! They only evaluate expr2 when it's needed, and in many cases, it's not needed. The classic usage is to avoid throwing exceptions in the unneeded evaluation of expr2, such as x <= array.length && array[x] == y. But they are also useful for a) avoiding a potentially expensive calculation of expr2 b) avoiding any unwanted side-effects in expr2. Of course if the programmer definitely wants the side-effects in expr2 to occur irrespective of expr1, I would submit that the code is in need of a review. Failing that, the programmer should use the strict version (& or |) and clearly document why strict evaluation is necessary. Your example of a void function with a single lazy argument isn't what Lasse suggested: the first argument (or this for the example member function given) is always evaluated, the subsequent arguments might or might not be, depending on the first one. Lasse's suggestion:
illustrates perfectly how you could define such an operator. The syntax on usage is very simple:
FWIW I have a strong bias towards coding non-void functions and methods with absolutely no side-effects. It makes reasoning about programs, debugging, and the job of an automated verification easier. And if you follow these principles, there are no side-effects to worry about with short-circuit operators or lazy arguments. See for example: command-query . As we are drifting off-topic into software engineering philosophy we should probably continue in a different thread or agree to differ :) |
I disagree. In practice, believing that a developer will adhere to some arbitrary rules never works, and there will always be someone who tries to do a bit too much with what they are given. They will try to be clever, thinking You have to think about the whole language, not just a part. I know that the PS: I know what lazy evaluation is ( |
Would it be possible to implement a short circuit logical boolean 'implies' operator (as in Eiffel and some other languages):
true imp false evaluates to false
true imp true evaluates to true
false imp false short circuits to true
I realise that Dart doesn't have alphabetic operators (I wish!) and that most non-alphanumeric character combinations are already assigned to operators, but maybe '!|' is available? It has two characters like the other logical short-circuit operators && and ||, and is strongly redolent of the equivalent operation which is !a || b. I hope this would not lead to ambiguities, but if so perhaps some other combination such as '|!', '^|', or '~|' might be possible.
Various work-arounds are:
// arguably more cryptic
// wordier and surely more cryptic
// doesn't short circuit, doesn't mimic an operator, not inlined (?) by the compiler
// expensive, unintuitive to call: imp(a, ()=>b), doesn't mimic an operator, unlikely to be optimised by the compiler
// mimics operator better: a.imp(b), otherwise same disadvantages as item 3)
// mimics operator better: a.imp(()=>b), otherwise same disadvantages as 4)
I believe the main reason why programmers aren't aware of the implies operator is because most languages don't have it. However, given the chance to use it - especially for contracts and assertions - you wouldn't want to do without.
For as discussion, see
https://softwareengineering.stackexchange.com/questions/184089/why-dont-languages-include-implication-as-a-logical-operator
Thanks for your comments/feedback
The text was updated successfully, but these errors were encountered: