Skip to content
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

Allow Overloading || and && #2722

Closed
wants to merge 3 commits into from

Conversation

Nokel81
Copy link
Contributor

@Nokel81 Nokel81 commented Jul 11, 2019

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>

Rendered

@RustyYato
Copy link

Rendered

@comex
Copy link

comex commented Jul 11, 2019

    /// 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;
}

@RustyYato
Copy link

RustyYato commented Jul 11, 2019

@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 ||

@comex
Copy link

comex commented Jul 11, 2019

@KrishnaSannasi Good point, I like that better.

@Nokel81
Copy link
Contributor Author

Nokel81 commented Jul 11, 2019

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)

@RustyYato
Copy link

RustyYato commented Jul 11, 2019

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:
Copy link
Member

@scottmcm scottmcm Jul 12, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 lets 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;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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...

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we have a distinct long type (intermediate) then the short_circuit for Result would still just be rhs.

@comex
Copy link

comex commented Jul 12, 2019

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.

@Centril Centril added T-lang Relevant to the language team, which will review and decide on the RFC. T-libs-api Relevant to the library API team, which will review and decide on the RFC. A-operator Operators related proposals. A-control-flow Proposals relating to control flow. A-impls-libstd Standard library implementations related proposals. A-traits-libstd Standard library trait related proposals & ideas A-expressions Term language related proposals & ideas labels Jul 12, 2019
2. Could lead to similarities to C++'s Operator bool() which => truthiness and is undesirable.

# Rationale and alternatives
[rationale-and-alternatives]: #rationale-and-alternatives
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 ||

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't understand the two traits question. Is it really necessary to discuss why each operator should have its own trait?

@Centril
Copy link
Contributor

Centril commented Jul 12, 2019

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.)

@Centril
Copy link
Contributor

Centril commented Jul 12, 2019

Another more meta point...

...How does this fit into the roadmap?

@scottmcm
Copy link
Member

(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.)

@RustyYato
Copy link

@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.

@Nokel81
Copy link
Contributor Author

Nokel81 commented Jul 12, 2019

Also, shouldn't we follow precedent of Option::and and Option::or to allow Option<T> && Option<U> and similarly for Result<T, E>

Option<T>::or does not allow calling with Option<U>. However, since and does, then yes we should

@Nokel81
Copy link
Contributor Author

Nokel81 commented Jul 12, 2019

@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.

@Nokel81
Copy link
Contributor Author

Nokel81 commented Jul 12, 2019

How is thing 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.)

This being strange because it is an odd way to provide a default value?

@kennytm
Copy link
Member

kennytm commented Jul 12, 2019

(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.

@RustyYato
Copy link

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
Copy link
Contributor

Centril commented Jul 12, 2019

@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>"?

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 + for i32 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.

@burdges
Copy link

burdges commented Aug 19, 2019

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(|| ..).

@KodrAus KodrAus added the Libs-Tracked Libs issues that are tracked on the team's project board. label Jul 29, 2020
Comment on lines +100 to +113
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
Copy link
Contributor

@pickfire pickfire Jul 30, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
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
// or
Some(4) || Some(5); // == Some(4)
None || Some(5); // == Some(5)
// or_else
Some(4) || foo(); // == Some(4) (foo is *not* called)
None || foo(); // == Some(3) (foo is called)
// unwrap_or
None || 3; // == 3
Some(2) || 1; // == 2
// weird
Some(1) || panic!() // == Some(1) + These two are side effects from !
None || return // returns from function + and are the same to how boolean || works
// and
Some(2) && Some(3) // Some(3)
None && Some(1) // None
Some(3) && None // None
// and_then (without taking parameter)
Some(4) && foo(); // None (foo is called)
None && foo(); // None (foo is **not** called)
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
    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")

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Some(3) || return

Some(3) || return == Some(3) similar to how Some(1) || panic!() == Some(1)

How about Option to Result type? And vice versa?

There are already functions to do that conversion, so it doesn't seem necessary.

Comment on lines +126 to +131
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
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
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
// or
Ok(4) || Ok(5); // == Ok(4)
Err(MyError) || Ok(5); // == Ok(5)
// or_else (without taking parameter)
Ok(4) || foo(); // == Ok(4) (foo is *not* called)
Err(MyError) || foo(); // == Ok(3) (foo is called)
// unwrap_or
Err(MyError) || 3; // == 3
Ok(2) || 1; // == 2
// unwrap_or_else (without taking parameter)
Ok(4) || bar(); // == 4 (foo is *not* called)
Err(MyError) || bar(); // == 3 (foo is called)

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
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
1. Leaves the `||` and the `&&` as not strictly boolean operators, which might hurt readability
1. Leaves the `||` and the `&&` as not strictly boolean operators, which might hurt readability.

Comment on lines +143 to +146
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.
Copy link
Contributor

@pickfire pickfire Jul 30, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

solution (you need to read the source code to find this)
class OTP(object):
    """
    Base class for OTP handlers.
    """
    def __init__(
        self,
        s: str,
        digits: int = 6,
        digest: Any = hashlib.sha1,
        name: Optional[str] = None,
        issuer: Optional[str] = None
    ) -> None:
        """
        :param s: secret in base32 format
        :param digits: number of integers in the OTP. Some apps expect this to be 6 digits, others support more.
        :param digest: digest function to use in the HMAC (expected to be sha1)
        :param name: account name
        :param issuer: issuer
        """
        self.digits = digits
        self.digest = digest
        self.secret = s
        self.name = name or 'Secret'
        self.issuer = issuer

@scottmcm scottmcm self-assigned this Sep 2, 2020
@scottmcm
Copy link
Member

scottmcm commented May 4, 2021

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
}

@H2CO3
Copy link

H2CO3 commented May 4, 2021

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));
}

@PoignardAzur
Copy link

PoignardAzur commented May 5, 2021

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();

as

let x = try {
  some_option?;
  other_option?;
  get_an_option()
}

@scottmcm
Copy link
Member

scottmcm commented May 5, 2021

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.

@scottmcm
Copy link
Member

scottmcm commented May 5, 2021

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.)

@rfcbot
Copy link
Collaborator

rfcbot commented May 5, 2021

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 rfcbot added proposed-final-comment-period Currently awaiting signoff of all team members in order to enter the final comment period. disposition-postpone This RFC is in PFCP or FCP with a disposition to postpone it. labels May 5, 2021
@nikomatsakis
Copy link
Contributor

@rfcbot reviewed

@rfcbot rfcbot added final-comment-period Will be merged/postponed/closed in ~10 calendar days unless new substational objections are raised. and removed proposed-final-comment-period Currently awaiting signoff of all team members in order to enter the final comment period. labels May 12, 2021
@rfcbot
Copy link
Collaborator

rfcbot commented May 12, 2021

🔔 This is now entering its final comment period, as per the review above. 🔔

@rfcbot rfcbot added the finished-final-comment-period The final comment period is finished for this RFC. label May 22, 2021
@rfcbot
Copy link
Collaborator

rfcbot commented May 22, 2021

The final comment period, with a disposition to postpone, as per the review above, is now complete.

As the automated representative of the governance process, I would like to thank the author for their work and everyone else who contributed.

The RFC is now postponed.

@rfcbot rfcbot added to-announce postponed RFCs that have been postponed and may be revisited at a later time. and removed final-comment-period Will be merged/postponed/closed in ~10 calendar days unless new substational objections are raised. disposition-postpone This RFC is in PFCP or FCP with a disposition to postpone it. labels May 22, 2021
@rfcbot rfcbot closed this May 22, 2021
matthiaskrgr added a commit to matthiaskrgr/rust that referenced this pull request Dec 10, 2022
Remove wrong note for short circuiting operators

They *are* representable by traits, even if the short-circuiting behaviour requires a different approach than the non-short-circuiting operators. For an example proposal, see the postponed [RFC 2722](rust-lang/rfcs#2722). As it is not accurate, remove most of the note.
thomcc pushed a commit to tcdi/postgrestd that referenced this pull request May 31, 2023
Remove wrong note for short circuiting operators

They *are* representable by traits, even if the short-circuiting behaviour requires a different approach than the non-short-circuiting operators. For an example proposal, see the postponed [RFC 2722](rust-lang/rfcs#2722). As it is not accurate, remove most of the note.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
A-control-flow Proposals relating to control flow. A-expressions Term language related proposals & ideas A-impls-libstd Standard library implementations related proposals. A-operator Operators related proposals. A-traits-libstd Standard library trait related proposals & ideas finished-final-comment-period The final comment period is finished for this RFC. Libs-Tracked Libs issues that are tracked on the team's project board. postponed RFCs that have been postponed and may be revisited at a later time. T-lang Relevant to the language team, which will review and decide on the RFC. T-libs-api Relevant to the library API team, which will review and decide on the RFC. to-announce
Projects
None yet
Development

Successfully merging this pull request may close these issues.