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

The parameter ordering of Option::map_or_else is unintuitive. #1025

Closed
abonander opened this issue Mar 31, 2015 · 23 comments
Closed

The parameter ordering of Option::map_or_else is unintuitive. #1025

abonander opened this issue Mar 31, 2015 · 23 comments

Comments

@abonander
Copy link

The signature of Option<T>::map_or_else is as follows:

fn map_or_else<U, D: FnOnce() -> U, F: FnOnce(T) -> U>(self, def: D, f: F) -> U

This is highly unintuitive as the two closure parameters are in the reverse order of that suggested by the function's name. This means that anyone who tries to invoke it from memory, including myself earlier today, is going to run into a compiler error on the first try.

Example:

let first_word_letter_count = "hello world".words().next().map_or_else(
    |word| word.len(),
    || 0
);

The above makes sense, logically. If the value is there, map it and return the result. Otherwise (implying a secondary operation), call a closure which will produce a substitute value. Even the documentation supports this logical progression:

Applies a function to the contained value or computes a default.

However, trying to invoke the method this way will result in a compiler error because the no-arg closure is required to be first. The particular reasoning behind this design is not given, but I have found precedent in Haskell's maybe function:

maybe :: b -> (a -> b) -> Maybe a -> b

However, I don't think Haskell's intuition for parameter ordering can extend to Rust because Rust doesn't have currying, partial application, or (global) lazy evaluation. And even in Haskell the ordering isn't necessarily intuitive.

I am aware that this method and Option itself have both been marked as Stable. However, I believe minor unintuities like this add up and ultimately reflect poorly on the user experience of the language as a whole, and should be addressed before Rust hits 1.0.

I would like to note that I am willing to apply the effort to adjust this myself, as it's relatively trivial. However, because it's trivial, I am not sure if it requires an RFC or just a general community agreement.

@reem
Copy link

reem commented Mar 31, 2015

I agree that this ordering is unintuitive and it would be better if the Some case was first.

@frewsxcv
Copy link
Member

Yes. I get this wrong every single time. 👍

@emberian
Copy link
Member

To be honest I thought the order was always the other way around. Was it changed?

@abonander
Copy link
Author

@cmr I thought it was always the other way around too. That's what screwed me up and motivated me to open this issue.

@huonw
Copy link
Member

huonw commented Mar 31, 2015

The order is currently chosen to be internally consistent with map_or. map_or takes the "main" closure last because that is syntactically much nicer:

foo.map_or(some_value, |x| {
    bar();
    baz(x);
    qux();
})

vs.

foo.map_or(|x| {
    bar();
    baz(x);
    qux();
}, some_value)

This isn't to say that we shouldn't change it, but we shouldn't consider this in isolation.

@abonander
Copy link
Author

@huonw That's understandable but still rather unintuitive. I would prefer to make it look nicer with a minor style tweak:

foo.map_or(|x| {
        bar();
        baz(x);
        qux();
    },
    some_value
)

or even:

foo.map_or(
    |x| {
        bar();
        baz(x);
        qux();
    },
    some_value
)

This does get a little more right-drift but I think it's worth the concession for cleaner, more intuitive code. I use the docs all the time but if I have to consult them to figure out the right argument ordering for something as simple as an operation on a monad, it's going to severely impact my efficiency.

@lilyball
Copy link
Contributor

lilyball commented Apr 4, 2015

I don't think I've ever screwed this one up. It just seems natural to me that the default value comes first. Probably because map_or() is "more fundamental" than map_or_else, so to speak (it has the shorter name and comes first in the documentation, and I would not be surprised to learn that it is the more popular of the two methods in actual usage).

@crazymykl
Copy link

@kballard, but even map_or has odd ordering. I'd expect the arguments to be, in order: map through this, or give me that.

@Gankra
Copy link
Contributor

Gankra commented May 31, 2015

So obviously this can't be "fixed" in 1.x, and fixing it in 2.x would be a disaster. So... this bug isn't actionable?

@nagisa
Copy link
Member

nagisa commented May 31, 2015

@gankro I believe deprecating this for an alternative with a name that does not cause wrong associations with parameter ordering might make it actionable. But in general, ditto.

@tshepang
Copy link
Member

@gankro why would it be a disaster? People will get a compile error, and the fix is simple.

@frewsxcv
Copy link
Member

People will get a compile error, and the fix is simple.

This might be true, but it would be pretty devastating to have virtually all Rust projects break just because some people aren't satisfied with the naming of argument order a method

@tshepang
Copy link
Member

@frewsxcv it's the re-ordering of arguments, not the renaming. Besides, people are free to stay with an ancient tool if doing a simple fix is too much effort. Some of these fixes could be automated even, especially simple ones like this.

@Gankra
Copy link
Contributor

Gankra commented May 31, 2015

@tshepang This is not a good way for a language developer to act. Breaking client code in such a blatant way should not be taken so lightly.

@tshepang
Copy link
Member

@gankro I think "fixing mistakes and oversights" is supposed to be part of 2.x. Some (more important) stuff will be broken anyways, so we might as well break the smaller things as well. I think making things more intuitive is worth the pain.

@frewsxcv
Copy link
Member

fixing mistakes and oversights

As stated above in previous comments, the argument order is intentional; it is neither a mistake nor an oversight.

@tshepang
Copy link
Member

I hear you, but a mistake can happen whether or not there is intent. And as this issue has proven, people do not like this choice.

@Stebalien
Copy link
Contributor

Triage: ⛵.

@steveklabnik
Copy link
Member

Yup, this ship has sailed.

GrantJamesPowell added a commit to GrantJamesPowell/lib_table_top that referenced this issue Nov 28, 2021
<Rant>

`clippy::map_unwrap_or` is a bad lint for the following reasons

0.) `map` => `unwrap_or_else` is two methods but that's not bad! It's
  the composition of two super important functions on `Result`, which
  leads to the more important point...
1.) `map_or_else` has it's arguments in the wrong order, which makes it
  unintuitive especially for a niche function on `Result`

Original

```rust
VerifiedBuiltin::<T>::from_str("default")
  .map(SettingsPtr::from) // <- what I'm mapping
  .unwrap_or_else(|_| SettingsPtr::from(T::default()))
```

Clippy Suggestion

```rust
VerifiedBuiltin::<T>::from_str("default")
  .map_or_else(
    |_| SettingsPtr::from(T::default()), // <- the _else_ branch!
    SettingsPtr::from // <- What I'm mapping...
  )
```

`map_or_else` is wildly unintuitive, and this dude agrees with me.
rust-lang/rfcs#1025
</Rant>
dsherret added a commit to denoland/deno that referenced this issue Mar 15, 2023
These methods are confusing because the arguments are backwards. I feel
like they should have never been added to `Option<T>` and that clippy
should suggest rewriting to
`map(...).unwrap_or(...)`/`map(...).unwrap_or_else(|| ...)`

rust-lang/rfcs#1025
kt3k pushed a commit to denoland/deno that referenced this issue Mar 16, 2023
These methods are confusing because the arguments are backwards. I feel
like they should have never been added to `Option<T>` and that clippy
should suggest rewriting to
`map(...).unwrap_or(...)`/`map(...).unwrap_or_else(|| ...)`

rust-lang/rfcs#1025
@who-biz
Copy link

who-biz commented Aug 6, 2024

Just coming here to say the "unintuitive" folks are right. Love that you're working on Rust. But I've now seen a few threads (forums and git) where this has been completely ignored (by many of the same folks). Please listen to the engineers using your language. They have a differing perspective from your own. With that said, dialogue has been halpful

@abonander
Copy link
Author

abonander commented Aug 6, 2024

@who-biz as the person who opened this issue almost* ten years ago (!), yeah, this ship has sailed. The time to change it was before 1.0.

To change this now would either require:

  • Deprecating the method in favor of a replacement, and then what do you name that and how do you teach it?
    • Nevermind messing up everyone, including myself, who just learned to deal with the argument order as-is.
  • A breaking change to swap the arguments in Rust 2.0, which isn't even in the cards AFAIK

If you really want to, you can create an extension trait in your project to fix this yourself:

pub trait OptionExt<T> {
    fn map_or_else_v2<U>(
        self, 
        map: impl FnOnce(T) -> U, 
        or_else: impl FnOnce() -> U
    ) -> Option<U>;
}

impl<T> OptionExt<T> for Option<T> {
    fn map_or_else_v2<U>(
        self, 
        map: impl FnOnce(T) -> U, 
        or_else: impl FnOnce() -> U
    ) -> Option<U> {
        self.map_or_else(or_else, map)
    }
}

@Diggsey
Copy link
Contributor

Diggsey commented Aug 6, 2024

@who-biz it's not been ignored, it's simply not actionable. Rust has a commitment to backwards compatibility, and I can tell you engineers care far more about that than unintuitiveness.

@who-biz
Copy link

who-biz commented Aug 7, 2024

Yes, agreed on all points, at this point in time.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests