Github Allow Overloading || and && by Nokel81 · Pull Request #2722 · rus...
source link: https://github.com/rust-lang/rfcs/pull/2722
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
Conversation
This is an rfc for allowing the user-of-rust to overload ||
and &&
for their own types. And adding various implementations for Option<T>
and Result<T, E>
/// Decide whether the *logical or* should short-circuit /// or not based on its left-hand side argument. If so, /// return its final result, otherwise return the value /// that will get passed to `logical_or()` (normally this /// means returning self back, but you can change the value). fn short_circuit_or(self) -> ShortCircuit<Self::Output, Self>;
If we're going to have such an elaborate setup with an intermediate value, might as well allow the intermediate value to be a different type, so that short_circuit_or
can provide arbitrary information to logical_or
. Something like:
trait ShortCircuitOr<Rhs = Self>: Sized { type Intermediate: LogicalOr; fn short_circuit_or(self) -> ShortCircuit<Intermediate::Output, Intermediate>; } trait LogicalOr<Rhs = Self>: Sized { type Output; fn logical_or(self, rhs: Rhs) -> Self::Output; }
@Nokel81 Please provide the full implementation of the traits for Option<T>
and Result<T, E>
in the "Reference-level explanation" section
Also, shouldn't we follow precedent of Option::and
and Option::or
to allow Option<T> && Option<U>
and similarly for Result<T, E>
@comex We can simplify to
trait LogicalOr<Rhs = Self>: Sized { type Output; type Intermediate; fn short_circuit_or(self) -> ShortCircuit<Self::Output, Self::Intermediate>; fn logical_or(intermediate: Self::Intermediate, rhs: Rhs) -> Self::Output; }
We don't need two traits, that seems like needless bloat. If you don't want short-circuiting behavior then you should overload &
and |
instead of &&
and ||
@KrishnaSannasi Good point, I like that better.
Ah so, just have a short circuit trait? If that is the case, then why have the custom intermediate type?
Also, as mentioned in the rfc we discussed auto traits which is not how rust does things (Add is not required for AddAssign)
Ah so, just have a short circuit trait? If that is the case, then why have the custom intermediate type?
The intermediate type allows us to minimize coupling between the two functions, for example we could implement LogicalOr
for bool
like so
pub struct BoolShortCircuitFailure(()); impl LogicalOr for bool { type Output = bool; type Intermediate = BoolShortCircuitFailure; fn short_circuit_or(self) -> ShortCircuit<Self::Output, Self::Intermediate> { if self { ShortCircuit::Short(true) } else { ShortCircuit::Long(BoolShortCircuitFailure(())) } } fn logical_or(_: Self::Intermediate, rhs: Rhs) -> Self::Output { rhs } }
Here since ShortCircuitBool
is zero-sized and can only be made by bool
, we can force short-circuit behavior and make the second step more efficient/easier to optimize by eliding checks.
For comparison, here is the implementation for bool
with the current LogicalOr
impl LogicalOr for bool { type Output = bool; fn short_circuit_or(self) -> ShortCircuit<Self::Output, Self> { if self { ShortCircuit::Short(true) } else { ShortCircuit::Long(false) } } fn logical_or(self, rhs: Rhs) -> Self::Output { assert!(!self, "Failure to use `short_circuit_or` before calling `logical_or` is a bug"); rhs } }
The assert
is technically unnecessary, but will catch bugs. But if we used an intermediate type, we can statically prevent these kinds of bugs and elide these checks.
# Guide-level explanation
[guide-level-explanation]: #guide-level-explanation
This proposal starts with an enum definition and trait definitions for each of the operators:
scottmcm on Jul 12, 2019 •
Member
I don't think this is the appropriate start for a guide-level explanation. I think this section should look substantially more like the ?
description in the book: describe a common need, describe how it can be done manually with if let
, then describe how it's handled using the ||
/&&
operators to be more concise. I think this section should also emphasize the parallel with booleans -- how foo() && bar()
is if foo() { true } else { bar() }
and how that's the same pattern in the if let
s seen here.
Some examples of the parallel are in IRLO. Maybe use an example about how you can just say i < v.len() && v[i] > 0
instead of if i < v.len() { false } else { v[i] > 0 }
.
(It would probably not mention the trait definitions at all.)
/// Complete the *logical or* in case it did not short-circuit.
/// Normally this would just return `rhs`.
fn logical_or(self, rhs: Rhs) -> Self::Output;
scottmcm on Jul 12, 2019
Member
These definitions seem to discard information, so seem like they'd be less than optimal when writing an impl.
For example, if short_circuit_or
returns Short
for an Ok
, then logical_or
still gets passed the whole Result
, and would need to do unimplemented!()
or something in the Ok
arm instead of only being passed the Err
part.
If it were, instead, -> ShortCircuit<Self::Short, Self::Long>
, then it could be logical_or(Self::Long, Rhs)
. But of course then short_circuit
becomes exactly the same as Try::into_result
...
Nokel81 on Jul 15, 2019
Author
Contributor
If we have a distinct long type (intermediate) then the short_circuit
for Result
would still just be rhs
.
Well, I was thinking that determining whether to short circuit might require performing some expensive calculation, which might involve generating intermediate values which could be reused in determining the final output value.
...One might argue that that use case is uncompelling because having ||
perform an expensive calculation is a code smell anyway.
But given that the design has an intermediate value, it's a bit strange to require it to be the same type as Self
for no real reason. Especially with @KrishnaSannasi's version that doesn't require a whole other trait to allow the customization.
2. Could lead to similarities to C++'s Operator bool() which => truthiness and is undesirable.
# Rationale and alternatives
[rationale-and-alternatives]: #rationale-and-alternatives
scottmcm on Jul 12, 2019
Member
Two more things I'd like to see discussed in here
-
Why the method to allow combining the sides, vs just something like
-> Option<Rhs>
? -
Why two traits, vs using one method that splits into the two parts, one kept on
&&
and the other kept on||
Nokel81 on Jul 15, 2019
Author
Contributor
I don't understand the two traits question. Is it really necessary to discuss why each operator should have its own trait?
How is this RFC going to be compatible with rust-lang/rust#53667 given that you want to desugar &&
to a match
and then dispatch to traits? Presumably this will interfere with the compiler's ability to understand if let Some(x) = foo() && bar(x) {
as introducing bindings in x
. In particular, the proposal here is to lower &&
to match
which means that you cannot make semantic choices such as for let ps = e
based on type information (e.g. bool
) of the LHS and RHS. Moreover, using match
+ dispatch to trait methods seems like the sort of thing that would regress compile time performance non-trivially.
(I also think that allowing Some(2) || 1
is rather semantically strange.)
Another more meta point...
...How does this fit into the roadmap?
(I also think that allowing Some(2) || 1 is rather semantically strange.)
@Centril In the sense of "nobody would ever write that" or in the sense of "I don't think || 0
is a good way to provide a default value for an Option<i32>
"?
As for #53667, doesn't it also understand the scoping of bindings by desugaring the control flow? Is there an indication that this control flow desugar would be different from that one? (Also, the compiler can special-case &&
for bool the same way it special-cases +
for i32
and []
for arrays.)
@Centril I thought that
if let Some(x) = foo() && bar(x) {
would desugar to something like
if let Some(x) = foo() { if bar(x) { } }
Because of the let ...
binding's desugaring would take precedence over the normal &&
desugaring.
Moreover, using match + dispatch to trait methods seems like the sort of thing that would regress compile time performance non-trivially.
Like @scottmcm said, we could special case certain types (bool
, maybe others) to improve compile times, so I don't see this as a big issue.
(I also think that allowing Some(2) || 1 is rather semantically strange.)
I find this just as strange as allowing true || false
, so I'm curious as to why you think that it is strange.
Also, shouldn't we follow precedent of
Option::and
andOption::or
to allowOption<T> && Option<U>
and similarly forResult<T, E>
Option<T>::or
does not allow calling with Option<U>
. However, since and
does, then yes we should
@KrishnaSannasi As for the having a "short circuit" trait and then relying on the &
and |
does not follow from the rest of the operator precidents. Add
does not automatically imply AddAssign
.
How is thing RFC going to be compatible with rust-lang/rust#53667 given that you want to desugar
&&
to amatch
and then dispatch to traits? Presumably this will interfere with the compiler's ability to understandif let Some(x) = foo() && bar(x) {
as introducing bindings inx
. In particular, the proposal here is to lower&&
tomatch
which means that you cannot make semantic choices such as forlet ps = e
based on type information (e.g.bool
) of the LHS and RHS. Moreover, usingmatch
+ dispatch to trait methods seems like the sort of thing that would regress compile time performance non-trivially.(I also think that allowing
Some(2) || 1
is rather semantically strange.)
This being strange because it is an odd way to provide a default value?
(I also think that allowing
Some(2) || 1
is rather semantically strange.)I find this just as strange as allowing
true || false
, so I'm curious as to why you think that it is strange.
The issue is that having both (Option<T> || Option<T>) → Option<T>
and (Option<T> || T) → T
is semantically strange. I don't see how true || false
((bool || bool) → bool
) is relevant.
As for the having a "short circuit" trait and then relying on the & and | does not follow from the rest of the operator precidents. Add does not automatically imply AddAssign.
That's not what I meant, I meant that if someone wanted to not use short-circuit behavior they should use &
and |
, and not use &&
and ||
.
@Centril In the sense of "nobody would ever write that" or in the sense of "I don't think
|| 0
is a good way to provide a default value for anOption<i32>
"?
Neither of those reasons really.
I find this just as strange as allowing
true || false
, so I'm curious as to why you think that it is strange.
I think that a binary operator like this taking an expression of a different type is peculiar and surprising. I'm aware that +
et. al allows Rhs
to be differently typed but that's mostly to allow similar-ish types and not something entirely different like T
as compared to Option<T>
. @kennytm also echoes my sentiments here.
Also, the compiler can special-case
&&
for bool the same way it special-cases+
fori32
and[]
for arrays.)
Like @scottmcm said, we could special case certain types (
bool
, maybe others) to improve compile times, so I don't see this as a big issue.
This special casing would need to happen after you have type information. Right now, it is simply assumed in fn check_binop
that each sides of the operands are coercible to bool
rather than using overloading. However, type checking match
also happens in the same phase of the compiler. If you want to use match
then you'll need to do a desugaring in fn lower_expr
which is before the type information you need is available. To get around this you would need to avoid lowering to match
and instead insert special logic into the type checker instead to handle it as the other overloaded operators are. This would presumably be substantially more complicated.
As for #53667, doesn't it also understand the scoping of bindings by desugaring the control flow? Is there an indication that this control flow desugar would be different from that one?
@Centril I thought that
if let Some(x) = foo() && bar(x) {
would desugar to something like
if let Some(x) = foo() { if bar(x) { } }Because of the
let ...
binding's desugaring would take precedence over the normal&&
desugaring.
Currently the compiler has an ast::ExprKind::Let
to encode let ps = e
syntactically. However, this is not really the issue.
Rather, the problem here is drop order. More specifically, an expression a && b && c
associates as (a && b) && c
but we drop temporaries here as b c a
and not a b c
. Moreover, if $cond { ... }
and while $cond { ... }
have the particular semantics that they make the $cond
a terminating scope. Because of these things combined, if you take something like if a && b
and lower it to match
as above then you will alter the drop order of existing things to a b c
instead (which is breaking) which I currently believe is necessary to have bindings work. If you instead desugar (a && b) && c
as desugar(desugar(a && b) && c)
I think you would instead not have bindings work. (Here is a few examples wrt. what happens with the drop orders, https://play.rust-lang.org/?version=nightly&mode=debug&edition=2018&gist=1170f09002025c40d77cd017717e1424) My current theory is therefore that if
+ let
+ &&
are not actually implementable with match
+ DropTemps
and that hir::ExprKind::{Let, If}
is needed. (Sorry about the dense implementation oriented description, but I don't have the time right now to elaborate in a more high-level fashion.)
At minimum I think the implementation of let_chains
needs to be finished before starting and accepting any work on this RFC.
Those are both comments about rust drawing heavily on functional languages.
I think foo() && { .. break; .. } && bar()
would normally be quite poor style.
Yes I feel that it can get wordy but it is also not possible to control the control flow of the enclosing function from within the closure.
That's a feature, I don't want to navigate code bases where I need to wonder what's the return type of a simple &&
or ||
expression, and I don't want to have to visually scan its right-hand side for control flow.
I would be against adding the ability to overload logical operators.
Looking at std::ops
as it looks currently, the traits in there can be categorized as:
- Math-operations: Add, AddAssign, Div, DivAssign, Mul, MulAssign, Neg, Rem,
RemAssign, Sub, SubAssign - Bit-operations: BitAnd, BitAndAssign, BitOr, BitOrAssign, BitXor, BitXorAssign
- Shift-operations: Shl, ShlAssign, Shr, ShrAssign
- Smart-pointers: Deref, DerefMut
- Destructors: Drop
- Closures: Fn, FnMut, FnOnce
- Indexed element access: Index, IndexMut
- Negation: Not
- Range abstraction: RangeBounds
- Related traits that aren't used directly: CoerceUnsized, DispatchFromDyn
- I have no idea what's
std::ops
about this: Generator - Propagation conversion: Try
In general, this matches my preferences. Overloaded operations are there to bring
custom user types in line with standard or builtin types or to provide an abstract
interface for operations when there are no fundamentally inherent types to the operation.
I would argue that this is not true for an overloaded ||
. It would be in line if instead there
was a Truth
trait that allowed overloading truthiness itself, which I would also consider a
bad idea.
I believe that logical operations should (at least strive to) uphold certain properties. For
example:
if a { if b { c() } }
and its relation to
if a && b { c() }
which would be broken by this RFC. I have similar feelings towards the math operations and
consider overloading +
for "putting things together" a mistake.
Since this change wouldn't allow overloading the truthiness of values, but only their usage
with certain logical operations, it would turn a set of purely logical operations into general
control flow operations that no longer relate to boolean logic. As was noted earlier in the thread,
I'd agree that things like some_condition || return
are less clear than their full if
based
counterparts and should be avoided. It seems to me that the change proposed in this RFC would
instead encourage coding like that. In this sense, .or_else(|| ...)
avoiding control flow
alterations can be seen as a feature.
There is a std::ops::Not
, but that doesn't really deal with boolean logic by itself. It just
happens to also work with bools. To be honest, I never used !
outside of booleans. I'd probably
consider using !
outside of a boolean context as misleading, and would .not()
instead.
I'd also like to note that I consider (paraphrased) "The std::ops
module is already big"
not an argument for making it bigger. This sentiment to me looks like embracing the slippery
slope full-on. It's becoming a more commonly seen argument these days and I wish we'd stay clear
of it. I'm also not sure it even works in this case, as std::ops
contains a bit more than just overloadable operator traits.
In general it is my view that overloading operations always makes code less obvious and logic
less evident, except in the cases where we bring new types into a family of types. Like being
able to give big integer implementations the ability to do usual math operations.
I do want to say that I understand the general sentiment of wanting a feature like this. I just
consider making existing logical operations more "vague" to be an unfit solution. I believe
something like postfix macros would be a much better fit for this functionality.
@phaylon That is a very well thought out argument. And yes, I do appreciate your understanding for wanting a feature like this. I do agree that postfix macros would fix this because then we could do a.or!(b).or!(c)
. But even then we would need some traits to make it generic enough to be useful (instead of a.opt_or!(b).opt_or!(c)
).
But even then we would need some traits to make it generic enough to be useful (instead of
a.opt_or!(b).opt_or!(c)
).
There is nothing wrong with making traits for combinators around Option<T>
and Result<T, E>
, just don't make those traits overload boolean operators.
Short circuit operators at the very least still should be added for at the very least custom types. Making a library to perform external operations, say BLAS or so, being forced to use something like .or(|| ...)
is not at all remotely clean when the math and language of it dictates short-circuiting &&
/||
and so forth. Whether people use them for other purposes should be left up to them and their own decision abilities.
Short circuit operators at the very least still should be added for at the very least custom types. Making a library to perform external operations, say BLAS or so, being forced to use something like
.or(|| ...)
is not at all remotely clean when the math and language of it dictates short-circuiting&&
/||
and so forth. Whether people use them for other purposes should be left up to them and their own decision abilities.
You can just be making your own local macros for whatever control flow structures you want to implement BLAS, that sounds like a bad argument to me, and the vast majority of people out there are not reimplementing BLAS anyway.
By that argument, I should have gotten a let … else
expression years ago given how much I could use that in Servo.
By that argument, I should have gotten a let … else expression years ago given how much I could use that in Servo.
I'm guessing for matching failures? Wouldn't if let ... else ...
be functionally the same, or do you mean for some other features.
In addition, this isn't for implementing new syntax, it's just for allowing existing syntax to work on custom types instead of being exceedingly specific enough to be near useless for all but the trivial cases.
After just reading the title and not the whole discussion on this RFC, I'd vote against that feature.
This results in even more cognitive overhead when using the language. That is exactly the overhead I tried to avoid when going for Rust instead of C++. The language has enough complexity already, this just allows the user to increase congnitive load when reading and writing Rust and does not help with readability and easy of use.
Edit: After reading the RFC more carefully, I can only say this even louder: I'm against this change in the language because it does increase the cognitive load and the "special-character/alphabet-character" ratio (which is undersirable). Having named functions for these tasks makes the language more expressive and more readable (as in 'I can read the code like english'). Changing this therefore will (IMO) result in less readability, more "having to keep things in mind"yiness and more confusion for beginners and maybe even intermediate Rust programmers.
Long story short: Please don't do this.
I'm still of the mind that this isn't a bad change, but should not be encouraged by the community, perhaps not even a default implementation on option/result, it should be kept for DSEL's.
I would also like to add that the function based system is not a zero cost abstraction. As I have thought about it longer (and listening to the people who do not like this idea) I have realized that postfix macros would be another alternative to this problem. They would be a zero cost abstraction, there would be no need to have the two methods which would also reduce the cognitive load. A factor that many people have brought up. Postfix macros would need to solve the type problem (a macro on both Result<T, E>
and Option<T>
using the same name)
Example:
let x = cmd_line .args() .get("-o") .or!(config.get("output")) .unwrap_or!("terminal");
Might expand to something like the following:
let x = { if let Some(res) = cmd_line .args() .get("-o") { res } else { if let Some(res) = config.get("output") { res } else { "terminal" } } }
As repeatedly stated above, f().and_then(|| ..)
must be a zero cost because otherwise rust's zero cost abstraction story falls apart everywhere, like iter.map(|| ..)
.
Some(4) || Some(5); // == Some(4)
None || Some(5); // == Some(5)
Some(4) || foo(); // == Some(4) (foo is *not* called)
None || foo(); // == Some(3) (foo is called)
None || 3; // == 3
Some(2) || 1; // == 2
Some(1) || panic!() // == Some(1) + These two are side effects from !
None || return // returns from function + and are the same to how boolean || works
Some(2) && Some(3) // Some(3)
None && Some(1) // None
Some(3) && None // None
Some(2) || Some("hello") // Error: LogicalOr<Option<&str>> not implemented for Option<i32>
Some(2) || 2 || 3 // Error: LogicalOr<i32> is not implemented for i32
Comment on lines
+100 to +113
pickfire on Jul 30, 2020 •
Contributor
Some(1) || panic!() // == Some(1) + These two are side effects from !
None || return // returns from function + and are the same to how boolean || works
Looks like magic, what are these? How about Some(3) || return
?
How about Option
to Result
type? And vice versa?
Some(4) && Ok(5)
None && Ok(5)
Some(4) && Err("no")
None && Err("no")
Some(4) || Ok(5)
None || Ok(5)
Some(4) || Err("no")
None || Err("no")
RustyYato on Dec 21, 2020
Some(3) || return
Some(3) || return == Some(3)
similar to how Some(1) || panic!() == Some(1)
How about
Option
toResult
type? And vice versa?
There are already functions to do that conversion, so it doesn't seem necessary.
Ok(4) || Ok(5); // == Ok(4)
Err(MyError) || Ok(5); // == Ok(5)
Ok(4) || foo(); // == Ok(4) (foo is *not* called)
Err(MyError) || foo(); // == Ok(3) (foo is called)
Err(MyError) || 3; // == 3
Ok(2) || 1; // == 2
Comment on lines
+126 to +131
pickfire on Jul 30, 2020
Contributor
bar()
returns i32
, but I think this is kinda hard to distinguish if xxx
in || xxx()
is returning what in the first place. It do two tasks which are either unwrap_
or or_
. Looks like a potential place for footguns.
# Drawbacks
[drawbacks]: #drawbacks
1. Leaves the `||` and the `&&` as not strictly boolean operators, which might hurt readability
pickfire on Jul 30, 2020
Contributor
This RFC also proposes to deprecate the `.or(...)`, `.or_with(|| ...)`, `.and(...)`, `.and_with(||
...)`, `.unwrap_or(...)`, and `.unwrap_or_with(|| ...)` methods on `Option<T>` and `.or(...)`,
`.or_with(|| ...)`, `.unwrap_or(...)`, and `.unwrap_or_with(|| ...)` methods on `Result<T, E>` since
using this feature renders them unneeded.
Comment on lines
+143 to +146
pickfire on Jul 30, 2020 •
Contributor
I think it may be a good idea to allow binary operator overloading but I don't think it is a nice idea to mix both or_
and unwrap_
functions into the same operator, it may be hard to distinguish if it is behaving like which function since it does it implicitly, it also requires the developer to know the context on the output of the function in order to understand how is &&
and ||
behaving.
I think keeping &&
and ||
for only or
and and
without those unwrap_
might be a good idea for Option
and Result
. It reduces the load on the user mind to get to understand whether it behaves like or_
or unwrap_
, you never know, you need to always double check, changing one function signature (Option<T>
to T
) might break all other parts of the code using &&
and ||
(chain of destruction).
Prior art (bad art):
- python
**kwargs
implicitness, results in bad docs and mental load, similar to this in the sense that it does multiple stuff
class HOTP(OTP): """ Handler for HMAC-based OTP counters. """ def __init__(self, *args: Any, initial_count: int = 0, **kwargs: Any) -> None: """ :param initial_count: starting HMAC counter value, defaults to 0 """ self.initial_count = initial_count super(HOTP, self).__init__(*args, **kwargs)
Guess what could you put for kwargs
without clicking on the item below.
Coming back to this after #3058, some thoughts. (Without taking a position on whether we should or shouldn't do this.)
I see in this RFC the following:
enum ShortCircuit<S, L> { Short(S), Long(L), } trait LogicalOr { type Output; type Intermediate; fn short_circuit_or(self) -> ShortCircuit<Self::Output, Intermediate>; }
And that leaps out to me as being nearly identical to the now-accepted
enum ControlFlow<B, C> { Break(B), Continue(C), } trait Try { type Output; type Residual; fn branch(self) -> ControlFlow<Self::Residual, Self::Output>; }
(Yes, I've left some details out from both, to focus things.)
Is this parallel meaningful? Certainly many of the examples here are on ?
-supporting types like Option
and Result
.
For instance, a.or_else(|_| b)
on those two types is equivalent, using ?
's new underlying trait, to
match a.branch() { ControlFlow::Continue(c) => c, ControlFlow::Break(_) => b }
Copy link
H2CO3 commented 12 days ago
The parallel certainly exists; I'm however still not sure how one would desugar the following to this form:
fn foo(x: bool) -> u32 { let _: bool = x || return 1337; 42 } fn main() { println!("{}", foo(false)); println!("{}", foo(true)); }
Skimming through the debate, I think the arguments for and against this feature boil down to one trade off:
Keeping the language easy to learn vs Making a specific kind of code easier to read once you know the language.
(where "easy to learn" is relative, because, well, this is rust)
I think logical operator overloading has strong benefits for visibility, if people are already used to them. YMMV, but when you read this code:
foo.bar().or(baz()); foo.bar() || baz;
The first line has four identifiers that the brain needs to "parse"; it needs to register that "or" has a different meaning than the other identifiers, and represents "structure".
In the second line, the structure is easier to read at a skim. It's clear that "foo.bar()" and "baz()" are two terms of an operation, and people familiar with ||
immediately understand the sort-circuiting part.
On the other hand, adding logical operators means adding another small bit of trivia that someone has to learn before they can understand any code they read. For a language concerned with feature creep, that's no small thing.
I was initially for this feature, but re-framing things this way is making me change my mind. I don't think logical operators are orthogonal enough with other features to justify the added complexity.
Besides, I think most of the use cases for logical operators are covered by try blocks. Eg I think you could rewrite
let x = some_option && other_option && get_an_option();
let x = try { some_option?; other_option?; get_an_option() }
On the other hand, adding logical operators means adding another small bit of trivia that someone has to learn before they can understand any code they read.
While that's certainly true in the abstract, it's not enough on its own, for me. How does it actually weigh out in practice for Rust code? Is it actually worse than learning "well, there's an or
method, but you probably don't want .or(foo())
"? Even ||
is another bit of trivia that people have to learn; it doesn't really need to exist either. a || b
could just be if a { true } else { b }
-- it's not horrible that way. (Or one could use the corresponding match
, since if
isn't needed either.)
Many languages have come to the conclusion that something here is valuable, like
And while those do work on null, the C# one at least works on Nullable<_>
too. They all could have been functions in those languages too (AFAIK? C# definitely could; I'm not certain about the rest), but the operator was deemed worth it.
That said, they're not ||
and &&
, so it's not a perfect parallel. But it's also apparently helpful enough that it was added even without the "people familiar with || immediately understand" advantage you mentioned.
Now, regardless of my previous post, I do think this point from @PoignardAzur is important:
Besides, I think most of the use cases for logical operators are covered by try blocks.
I agree that we don't yet know how a stable try {}
would impact desires, design, or usefulness of these things. I agree that try { (a?, b?) }
might be surprisingly nice, and conveniently avoids a bunch of the design questions here -- for example, try { a? & b? }
would also work -- so I think we need to learn more about that before we decide anything here.
As such, let's postpone considering this for now:
@rfcbot fcp postpone
And, for clarity, I'm not saying that this exact RFC should necessarily come back. Maybe try
will cover everything and nothing will be needed. Maybe something very similar to this (perhaps updated to ControlFlow
instead of ShortCircuit
) will end up being a good choice. Maybe it'll be a proposal for a different operator/macro/something.
(I don't know if that strictly means postpone or close, but whatever.)
Team member @scottmcm has proposed to postpone this. The next step is review by the rest of the tagged team members:
No concerns currently listed.
Once a majority of reviewers approve (and at most 2 approvals are outstanding), this will enter its final comment period. If you spot a major issue that hasn't been raised at any point in this process, please speak up!
See this document for info about what commands tagged team members can give me.
@rfcbot reviewed
Copy link
rfcbot commented 4 days ago
This is now entering its final comment period, as per the review above.
Recommend
About Joyk
Aggregate valuable and interesting links.
Joyk means Joy of geeK