The parameter ordering of `Option::map_or_else` is unintuitive


#1

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.

Opened as an issue on rust-lang/rfcs


#2

map_or_else's parameter ordering comes from the ordering of map_or, and the parameter ordering of map_or was chosen because it’s more ergonomic for functions that take multiple arguments where one argument is a closure to put the closure as the final argument, e.g

let val = someOpt.map_or(0, |x| {
    let count = foo(x);
    count * bar()
});

It does admittedly make map_or_else() look a little odd.


#3

map_or might look better that way, but being ergonomic isn’t just about looks. The function screams at me “why, oh, why?!” every time I encounter it. It breaks the principle of the least astonishment.


#4

Is there any precedent for other methods that have a single closure argument putting that argument anywhere except in tail position?

map_or may look slightly odd, but my belief is that if you pick any arbitrary function with a single closure argument and one or more non-closure arguments, the closure argument will be in tail position. I think matching that convention is more important than making map_or look less odd.


#5

Well, thanks for the explanation, knowing this titbit might make it easier for me to guess the corrent placing of arguments in the future.

lookup_hash in decoder.rs


#6

I suppose that technically answers my question, but I was talking about public API, not about private functions. Private functions don’t generally make as much of an effort to follow convention.


#7

You don’t have a lot of meaningful two-word-named two-arguments functions to establish a convention in the first place. “map_or” is very different from “fold”, you can’t look at functions like “fold” and establish some kind of convention for a two-word function like “map_or” from it because having a two-word function calls for a different argument placement.


#8

What’s your reasoning for this claim? This is the first time I’ve seen anyone assert that two-word function names have different conventions than other functions.


#9

Really? You mean you didn’t see the logician’s message above? “This is highly unintuitive as the two closure parameters are in the reverse order of that suggested by the function’s name


#10

logician did not claim that methods with two-word names were subject to different conventions than other methods, and I did not claim that nobody ever claimed that a given method’s parameters were put in what they considered to be a non-intuitive order. Straw-manning does not help you win debates.


#11

Look at the pot calling the kettle back.

There is an obvious intuitive principle that function names are related to parameter placement. Two-named functions are just an example or if you’d like a subset of that principle.

It’s not a debate, you doesn’t have a point you’re trying to prove, at least not explicitly stated, and I don’t see a need to prove something that obvious.

As for examples, here http://programmers.stackexchange.com/questions/240741/dealing-with-not-knowing-parameter-names-of-a-function-when-youre-calling-it you can see function names used as a hint to parameter placement.


#12

You have yet to explain why your claim that some particular parameter ordering is more intuitive based on the function name is worth breaking convention over. I find the claim that map_or makes more sense with the default parameter second to be plausible, but not more compelling than the argument that the closure argument should go last in order to match the convention that functions with one closure argument generally put that closure argument last. This is obviously a matter of opinion (despite your implication that your parameter ordering is objectively better), but one ordering (closure last) has the benefit of objective general convention and the other (default value last) is a subjective decision based on personal preference.


#13

And you have yet to prove that there is indeed a convention to place closure last to the exclusion of other design considerations.


#14

I’d also like to see some more proof (besides map_or and map_or_else) that there exists a convention to place the sole closure as the last argument in APIs. An acceptable evidence would be an API that has chosen to put the sole closure as the last argument even though some other location for it would make more sense. If such evidences are abundant, then that’s a proof that the convention exists.

And to further clarify: An API, where the sole closure is the last argument and where it logically makes sense for the closure to be the last argument, is not an indication that there exists a convention to put the sole closure as the last argument. We shouldn’t mistake “what makes the most sense” for a “convention”. For example, the ordering of the arguments of Iterator::fold makes sense because they’re in a chronological order, i.e. the function’s nascent result is first initialized to the former (value) argument and then the result is incrementally updated by using the later (closure) argument.


#15

We basically got this convention from history. We used to have do notation which was special syntax for calling functions with closures as the last argument. Hence no surprise that we tried to put closures last whenever possible. I think the syntactic gains are still there, even if the do notation is long gone.

do (1..10).fold(0) |acc, x| {
    acc + x
}

#16

Rust’s standard library at its current state either follows the following convention or it doesn’t:

“You should place a sole closure as the last argument even if it’s not the most logical and sensible placement for it.”

The history of Rust’s language design can only show us the reason why the standard might follow the convention, but the history is neither evidence nor proof that the standard actually follows the convention.


#17

Pick any such function in the standard library (other than Option::map_or or Option::map_or_else) and you’ll also notice that there’s no logical reason for the closure not to be the last argument. The standard library quite strictly follows the convention of: “all else being equal, prefer to put closure(s) as the last argument(s)”. But map_or and map_or_else are cases where all else is not equal, i.e. there’s a clear logical reason to put the arguments in a certain order.


#18

This is the claim I’m asking for evidence for. The only support for this claim is a subjective belief that a different argument order on a case-by-case basis makes more sense, but I don’t find that claim compelling. While I understand the argument that it makes sense to put the default value last, I don’t think it’s a strong enough claim to be worth breaking convention over. From my subjective viewpoint, the fact that map_or follows the “closure last” convention means I have never confused the parameter order there; I always assume closure-last, and since map_or follows that convention, I get the parameters correct without reading the docs.

It’s easy to find examples of Rust APIs where the closure goes last. For example, I just looked at std::iter::Iterator and the scan() and fold() methods follow this convention.

I haven’t exhaustively searched all of Rust, but I cannot think of—and have yet to be presented with—an API that breaks this convention (the sole example so far was not API, it was a private function inside of librustc).


#19

The Decoder trait in rustc-serialize also follows this pattern. It’s not eminently useful to implementors, but I’m sure it’s boon to whatever code implements the derive(RustDecodable) feature (or to anyone implementing a very custom Decodable).


#20

I think I remember reading some core team member saying that he’s not against argument labels in a future version of Rust. This could mean that calls to opt.map_or(value, fun) might change to opt.map(fun, or: value). This would change the argument order. Although I think I would prefer opt.map_if_some(fun, or: value) and opt.map_if_some(foo, or_map: bar).

To me it’s clearly more logical that any functions of the form <x>_or should take their arguments in the order of (<argument relating to x>, <argument relating to the "or" case>). And also, any function of the form <x>_or_<y> should take their arguments in the order of (<argument relating to x>, <argument relating to y>).

You misunderstood me. Let me clarify. You claim that there is this strong convention to use trailing closures come hell or high water. I want proof that this convention exists. Those examples you gave, scan() and fold(), and all the other examples that I can find from stdcore and stdlib folders (other than map_or and map_or_else), they only show that there’s a convention to use trailing closures when there’s no clear logical reason not to. And in fact, there’s a clear logical reason to put the value argument before the closure argument in both scan() and fold() because the value argument is used for initialization chronologically before the closure ever gets called. And since we read from left to right, there’s a clear chronologic aspect there which directly maps to the logic of the function.