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

Add methods for converting from `bool` to `Option<T>` #2757

Open
wants to merge 6 commits into
base: master
from

Conversation

@varkor
Copy link
Member

varkor commented Sep 5, 2019

Rendered.

The following pattern is prevalent in typical Rust code.

if some_condition {
    Some(t)
} else {
    None
}

Here, I propose new methods for converting a bool to an Option<T>, abstracting this pattern, to aid in readability and reducing repetition.

impl bool {
    fn to_option<T>(self, t: T) -> Option<T>;
    fn to_option_with<T, F: FnOnce() -> T>(self, f: F) -> Option<T>;
}

This supersedes #2180, which was closed due to concerns about the suggested names and resolves #2606.

text/0000-bool-to-option.md Outdated Show resolved Hide resolved
text/0000-bool-to-option.md Outdated Show resolved Hide resolved
@Centril

This comment has been minimized.

Copy link
Member

Centril commented Sep 5, 2019

Adding the language team since the actual implementation of this requires the addition of a language item #[lang = "bool"]. This should also be noted in the reference text of the RFC.

@Centril

This comment has been minimized.

Copy link
Member

Centril commented Sep 7, 2019

@varkor I think it would also be good to add a few examples of before/after for real world use-cases, e.g. in the compiler or elsewhere, that would have benefited from this RFC.

I think it would also be worth noting that this encourages the use of more closures. This ostensibly has the drawback of longer compile-times. On the flip-side, it also is a good idea due to the restrictions imposed on imperative control flow (return & friends). Such restrictions may improve local reasoning and is helpful towards creating closures that may be refactored into stand-alone functions over time. So the RFC may be a boon for maintainability.

@varkor

This comment has been minimized.

Copy link
Member Author

varkor commented Sep 7, 2019

There's now a reference implementation at rust-lang/rust#64255.

@fbstj

This comment has been minimized.

Copy link
Contributor

fbstj commented Sep 7, 2019

I remember previous discussions on similar topics have regularly brought up the question of symmetry; aka "what about when self is false". is it something that should be mentioned/discussed in the RFC? is it future enhancement? is it worth adding the dual false.unthen() or should there be a false.not().then() or does !false.then() have the correct precedence (I believe not because it has to be looser than method calls for most things), or is (!false).then() acceptable?

@Centril

This comment has been minimized.

Copy link
Member

Centril commented Sep 7, 2019

@fbstj https://doc.rust-lang.org/nightly/std/ops/trait.Not.html -- we could add it to the prelude if necessary.

I think foo.not().then(...) or (!foo).then(...) is fine.

@varkor

This comment has been minimized.

Copy link
Member Author

varkor commented Sep 7, 2019

Very often you can rewrite a condition to avoid negation, and if that's not convenient (if the condition is used elsewhere, for instance), negation with ! or .not() seems no less readable, so I don't think we need extra methods for this.

@Centril

This comment has been minimized.

Copy link
Member

Centril commented Sep 7, 2019

..., so I don't think we need extra methods for this.

And on the off chance we do, we are free to add more methods later, so this feels very safe. ;)

@Centril Centril added the I-nominated label Sep 7, 2019
Centril added a commit to Centril/rust that referenced this pull request Sep 7, 2019
Add methods for converting `bool` to `Option<T>`

This provides a reference implementation for rust-lang/rfcs#2757.
@troiganto

This comment has been minimized.

Copy link

troiganto commented Sep 7, 2019

I think the existing methods that are closest to the ones being proposed here are:

x.ok();  // Result<T, E> → Option<T>
x.err(); // Result<T, E> → Option<E>

x.ok_or(y);            // Option<T> → Result<T, E>
x.ok_or_else(closure); // Option<T> → Result<T, E>

x.and(y); x.and_then(closure); // Result<T, E> → Result<U, E>
x.or(y); x.or_else(closure);   // Result<T, E> → Result<T, F>

x.and(y); x.and_then(closure); // Option<T> → Option<U>
x.or(y); x.or_else(closure);   // Option<T> → Option<T>

I've noticed that the first four methods, which transform between different "monad kinds", explicitly mention the variant to transform to. The rest (and/and_then/or/or_else), on the other hand, stays within Option and Result respectively.

Based on this precedent, I think that methods which convert from bool to Option<T> should also contain the word "some" in their name, whatever the concrete choice is.

@varkor

This comment has been minimized.

Copy link
Member Author

varkor commented Sep 8, 2019

I've noticed that the first four methods, which transform between different "monad kinds", explicitly mention the variant to transform to.

Unfortunately, they're not that consistent. Both the methods from Option to Result and Result to Option are named after the Result (i.e. ok and err). This naming scheme in general does not extend well to other type combinations. I read some people state that some would be confusing, because it's either Some or None.

@camlorn

This comment has been minimized.

Copy link

camlorn commented Sep 8, 2019

Seeing the name debate, I just want to ask why not to_option and to_option_with? I imagine this was discussed elsewhere, but in terms of "what does this do" when you don't know them, my_bool.then(5) is cryptic, my_bool.to_option(5) isn't. The former might have nice systemic math-like implications but the latter is very clear about what it does at least in my opinion.

@nugend

This comment has been minimized.

Copy link

nugend commented Sep 11, 2019

Sorry, I skimmed through the discussion and didn't see it, but has something along the the following lines been considered? I realize that this goes back to putting the method on the Option rather than the bool and I didn't see if that had been ruled out entirely as unworkable in the earlier RFC.

Some(x).test(boolvar) // Some(x) or None

I don't know if there's a general rule against using test as a function name though (didn't see it in the keyword list). I was thinking ok might work as well, but then there's a small chance of confusion with ok_or and ok_or_else due to the different return types.

Presumably a lazy version would be a bit verbose though, which might make it a non-starter

Some(()).test(boolvar).and_then(|_| expensive_expression)
@joshtriplett

This comment has been minimized.

Copy link
Member

joshtriplett commented Sep 11, 2019

I'm not sure why this is tagged T-lang; this is entirely a libs decision.

@joshtriplett joshtriplett removed the T-lang label Sep 11, 2019
@joshtriplett

This comment has been minimized.

Copy link
Member

joshtriplett commented Sep 11, 2019

Ah, just saw @Centril's comment (which was marked as resolved).

For the sake of simplicity, I'd suggest treating the questions orthogonally; can we just ask if anyone on @rust-lang/lang objects to the creation of this lang item for bool, and if not, leave the rest of the decision entirely to T-libs?

@dtolnay

This comment has been minimized.

Copy link
Member

dtolnay commented Oct 21, 2019

I think the trouble with to_option in context was that the name contains "to" but it wasn't clear that there isn't anything serving the role of "from". From other methods like to_owned and to_string we are used to there being a thing we convert "from" and a thing we convert "to", but the methods here are more about if/then rather than from/to. In toy examples it was easy to look like we are converting "from" the boolean, but that perspective ended up not making sense in real code where the boolean is just a condition and it would be confusing to think of converting "from" it.


  • path.is_absolute().then_some(path_buf) means: if path.absolute() then Some(path_buf).

    .then_some(path_buf)
          ^^^^^^^^^^^^^^ what you see is what you get
    
  • path.is_absolute().then(|| PathBuf::from(path)) means: if condition is true then here is what to do. The "thing to do" (closure) either runs or not, and correspondingly you receive the Some if it ran or the None if it did not run.

@varkor

This comment has been minimized.

Copy link
Member Author

varkor commented Oct 21, 2019

Okay, I'm happy enough with then_some/then.

@troiganto

This comment has been minimized.

Copy link

troiganto commented Oct 21, 2019

Couldn't it be considered confusing that Option::then() takes a value, but bool::then() takes a closure? I'm wondering about the mental overlap here, since Option is involved in both functions.

@dtolnay

This comment has been minimized.

Copy link
Member

dtolnay commented Oct 21, 2019

@troiganto: there is no Option::then method so I don't understand what you are referring to. There is Option::and which takes a value and Option::and_then which takes a closure, so the use of ending-then for a closure is absolutely consistent with my recommended names. Your objection applies to @varkor's old names of then (value) / then_with (closure) but be aware that my names are then_some (value) / then (closure).

As a side note, English is hard so there are multiple meanings of "then". In the case of bool::then it means if/then -- if cond then do. In the case of Option::and_then it means first/then -- first acquire some value then do thing with it.

Finally, everyone is welcome to keep pointing out objections, but understand that the point isn't to find a set of names without downsides. If you know such names, by all means tell us, but otherwise the point is to have names that make code clearer when used, which then_some / then achieve but to_option does not (as seen in rust-lang/rust#65195).

@troiganto

This comment has been minimized.

Copy link

troiganto commented Oct 21, 2019

Ah, that was a mix-up on my part, sorry for the noise. You'll hear no further objections from me 👍

@varkor

This comment has been minimized.

Copy link
Member Author

varkor commented Oct 23, 2019

I've updated the names in the RFC to then_some and then, following @dtolnay's suggestion.

@withoutboats

This comment has been minimized.

Copy link
Contributor

withoutboats commented Oct 23, 2019

@rfcbot concern :((

Not sure whether to maintain this concern or not. I don't really feel optimistic that I can convince the Rust community these combinators are obscure and unhelpful, and I'm mostly resigned to std being full of them, since I will just never use them myself.

Like @dtolnay, I think the changes in #65195 made the code less clear, but unlike @dtolnay, I don't think changing the method name would help. Comparing this example:

if found { Some(cur) }
    else { None }
found.then(cur)

The second in my view is totally opaque, now I need to figure out the type of found, and know what its then method does, to figure out what this code is doing. I think the theoretical arguments for to_option are pretty compelling in this example, but the best solution is to just not use a method: with the if statement, everything is just right there for you: you know that found is a boolean and that the statement evaluates to an option without looking elsewhere.

I think the main objection that leads people to want these methods is that rustfmt styles these if statements awfully (readers will note I styled the above code irregularly), so you end up with things like this:

if fn_like {
    Some(FnLikeNode {
        node,
    })
} else {
    None
}

Whereas it will allow you to write the method form in a single line. This is the sort of reason I don't use rustfmt or any other automatic code formatter when I write code. I realize "don't use rustfmt" is a losing argument, but it seems rather misguided to make your code less self-evident because the your formatter is making it less pleasing, seems like a really clear failure of the formatter to me in this case.

I don't know what to do here. I'm probably inclined to just let this be stabilized under whatever name other people can reach consensus on and then personally almost never use this method. But I am really at odds with the attitude this method improves on the corresponding if statements in every or even most cases.

@withoutboats

This comment has been minimized.

Copy link
Contributor

withoutboats commented Oct 23, 2019

@rfcbot resolve :((

Don't think its worth blocking.

@djc

This comment has been minimized.

Copy link
Contributor

djc commented Oct 23, 2019

@withoutboats since you're alluding to "these combinators", do you mean you (would have) oppose(d) many of the combinators in std? I'd be interested in your broader argument/viewpoint on this, if you'd care to elaborate (not sure this is the best venue for it).

And, in thinking about the comments from withoutboats and dtolnay, I came up with one more color for the bikeshed that I don't think has been mentioned: Option::from_bool(path.is_absolute(), || path_buf). This has the advantage that the conversion is much more explicit since both Option and bool are called out explicitly. It also only has one variant, always taking the closure, at the downside of taking slightly more syntax for the direct expression case. (One alternative name that I came up with is inflate(), but that's probably too idiosyncratic for something like this.)

@Centril

This comment has been minimized.

Copy link
Member

Centril commented Oct 23, 2019

And, in thinking about the comments from withoutboats and dtolnay, I came up with one more color for the bikeshed that I don't think has been mentioned: Option::from_bool(path.is_absolute(), || path_buf). This has the advantage that the conversion is much more explicit since both Option and bool are called out explicitly.

Option::from_bool largely negates the chaining benefits that .then(...) comes with combined with being more verbose. I think that puts it under the threshold of usefulness to justify the addition of a lang item.

@SimonSapin

This comment has been minimized.

Copy link
Contributor

SimonSapin commented Oct 25, 2019

I agree that a non-chaining associated function would this less an improvement over the status quo, perhaps to the point that it’s not worth adding something to the standard library.

However I don’t think the lang item itself should require a lot of justification. That the standard library can define inherent methods on primitive types is an established fact that is not gonna change. We already have 21 lang items for this and they all work the same way. While coordinating with the compiler team and the language team when adding one more sounds fine, doing so would not add significant complexity to the compiler or the language. So the lang item count alone shouldn’t be an argument against a proposed addition to the standard library.

@nikomatsakis

This comment has been minimized.

Copy link
Contributor

nikomatsakis commented Oct 25, 2019

I tend to agree with @SimonSapin that lang team already "authorized" adding inherent methods to primitive types; I think that the libs team ought to be generally trusted to add appropriate methods. If this requires adding a lang item (I think that would be for the inherent impl?), that doesn't seem like a big deal in and of itself to me.

@withoutboats

This comment has been minimized.

Copy link
Contributor

withoutboats commented Oct 29, 2019

@withoutboats since you're alluding to "these combinators", do you mean you (would have) oppose(d) many of the combinators in std? I'd be interested in your broader argument/viewpoint on this, if you'd care to elaborate (not sure this is the best venue for it).

I'm not against adding this API (which is why decided to remove my concern) and I'm not against having the Option and Result combinator APIs in std overall, but I definitely do not think the message should be "anywhere you can use a combinator, you should." They usually do not make code easier to understand. Occasionally they do, and that's when people should use them. But in my experience that's very rare.

@camlorn

This comment has been minimized.

Copy link

camlorn commented Oct 29, 2019

Just a thought to contribute. I think there are broadly two types of programmers. Let's call them combinator programmers and imperative programmers (I don't want to use the word functional here because it's not exactly functional programming that I mean). I think that code in one style seems bad to people in the other camp but that neither style is bad as such in itself. I'm not trying to play the middle of an argument or somesuch because this is actually an important point in my opinion. Rust chose to support both styles of programming, and I think that therefore broadly speaking Rust should continue to support both styles of programming--in effect adding combinators where combinators can be added, but also improving the imperative constructs (though for the most part I view the imperative constructs as "finished" because overall I'm not sure what could be added).

For examples of languages which broadly chose one path over the other, Go chose imperative and JS promises/async stuff chose combinators (at least, for a long time until async/await happened).

If anyone's wondering I'm in the combinator camp/strategy.

@theduke

This comment has been minimized.

Copy link

theduke commented Nov 2, 2019

I concur with @withoutboats concerns here.

I don't think this is a question of combinators vs no combinators, as @camlorn framed it. I love to use them.

I do think that this is a case of a combinator that is helpful to have when looked at in isolation, but makes code more opaque and confusing in practice.

The naming is also somewhat contrarian to what is common in Rust: to_ methods are always used for direct, unconditional conversion, without requiring additional input. map_option could be a clearer alternative, but I don't see a different name really mitigates the confusing code.

I think the bar for entering std should be high, and additions should only be accepted if there is no clear downside.

[boolinator](https://docs.rs/boolinator/2.4.0/boolinator/)), but this functionality is commonly
desired and an obvious candidate for the standard library, where an external crate for such simple
functionality is not convenient.

This comment has been minimized.

Copy link
@fstirlitz

fstirlitz Nov 9, 2019

Might as well mention if some_condition { t } as an alternative.

This comment has been minimized.

Copy link
@varkor

varkor Nov 9, 2019

Author Member

Inlining a method body is always a possible alternative, so mentioning it specifically here isn't useful.

This comment has been minimized.

Copy link
@kennytm

kennytm Nov 9, 2019

Member

@varkor that does not mean inlining the method body, but to make if x { y } evaluates to Some(y) if x is true and None if x is false (basically an even more controversial version of https://internals.rust-lang.org/t/pre-rfc-break-with-value-in-for-while-loops/11208)

This comment has been minimized.

Copy link
@varkor

varkor Nov 9, 2019

Author Member

Ah, I see. Unless there are serious proponents of that approach, I don't think it's worth including, as it seems like an unviable alternative to me.

@Centril

This comment has been minimized.

Copy link
Member

Centril commented Nov 11, 2019

@dtolnay I believe @varkor addressed your concerns, so ping. :)

I tend to agree with @SimonSapin that lang team already "authorized" adding inherent methods to primitive types; I think that the libs team ought to be generally trusted to add appropriate methods. If this requires adding a lang item (I think that would be for the inherent impl?), that doesn't seem like a big deal in and of itself to me.

Well I agree that this lang item in particular is not a big deal (because as @SimonSapin notes, we already have a bunch of those for primitive types, and so in terms of spec complexity there's nothing novel here). However, I think it's important that institutional hindrances exist against the addition of lang items, and the language team is a good check on that. That said, as for adding a non-method, I didn't make a point about teams -- just that I personally didn't think it was worth it -- and it seems like there's agreement on that, so that's great. :)

@dtolnay

This comment has been minimized.

Copy link
Member

dtolnay commented Nov 11, 2019

@rfcbot fcp cancel

@rfcbot

This comment has been minimized.

Copy link

rfcbot commented Nov 11, 2019

@dtolnay proposal cancelled.

@dtolnay

This comment has been minimized.

Copy link
Member

dtolnay commented Nov 11, 2019

Proposing FCP to introduce unstable methods on bool for conditionally creating an Option::Some or running a closure.

This RFC has been unusual compared to other libs RFCs in that whether we accept these methods hinges on whether we can settle on sufficiently clear names for the behavior; it isn't like e.g. MaybeUninit where we know we need the functionality and we'll pick a name one way or another. As such, mostly the FCP is to confirm that we are interested in gathering experience using this set of names—if it seems to work out then we'll follow up in the tracking issue with a separate FCP to stabilize later.

impl bool {
    fn then_some<T>(self, t: T) -> Option<T> {
        if self {
            Some(t)
        } else {
            None
        }
    }

    fn then<T, F: FnOnce() -> T>(self, f: F) -> Option<T> {
        if self {
            Some(f())
        } else {
            None
        }
    }
}

This is the same functionality as from the earlier FCP in #2757 (comment), but new names as motivated in #2757 (comment).

@rfcbot fcp merge

@rfcbot

This comment has been minimized.

Copy link

rfcbot commented Nov 11, 2019

Team member @dtolnay has proposed to merge this. The next step is review by the rest of the tagged team members:

Concerns:

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.

@dtolnay

This comment has been minimized.

Copy link
Member

dtolnay commented Nov 11, 2019

@rfcbot concern :((

Registering withoutboats's concern from #2757 (comment).

@dtolnay

This comment has been minimized.

Copy link
Member

dtolnay commented Nov 11, 2019

@rfcbot resolve :((

@phaazon

This comment has been minimized.

Copy link
Contributor

phaazon commented Nov 12, 2019

Just to note, currently, I’m using the try-guard crate to do exactly that. I was happy to see that coming to std but it looks like it will not.

Just a thought to contribute. I think there are broadly two types of programmers. Let's call them combinator programmers and imperative programmers (I don't want to use the word functional here because it's not exactly functional programming that I mean). I think that code in one style seems bad to people in the other camp but that neither style is bad as such in itself.

Yes, I agree with that statement. However, as you said it, both styles are useful. I’m a Haskeller and use lots of combinators, but I also use lots of imperative statements. It’s not a “pick one; stick to it” decision. Especially, I use bool -> Option<T> / bool -> Result<T, E> to guard and verify pre-conditions and early-returns in functions. But for other common check, I tend to use if / else statements.

After reading the pro and cons arguments from this thread, I’m not sure what the best decision should be (I’m not saying that I’m against the decision of closing, I just haven’t made up my mind about how I feel about it). After all, we can still have those combinators in lib crates. The best comparison to me is, again, Haskell, because I know it a lot, but Haskell has that kind of functions in base but the Haskell ecosystem is also using a lot libraries such as either, errors, etc.

@SimonSapin

This comment has been minimized.

Copy link
Contributor

SimonSapin commented Nov 13, 2019

Proposing FCP to introduce unstable methods on bool for conditionally creating an Option::Some or running a closure.

Landing unstable methods has already happened in rust-lang/rust#64255 (tracked in rust-lang/rust#64260) so I’m not sure what this FCP is for. Deciding on names (or removal) is basically the only thing left to do.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
You can’t perform that action at this time.