Hello everyone!

In the last week I've been trying to update both std::Result and std::Option's API to match each other more, and I'd like to see the opinion of the community and the
Devs on a few areas it touches.

# Option today

The baseline here is Options current API, which roughly looks like this:

1. Methods for querying the variant
  - fn is_some(&self) -> bool
  - fn is_none(&self) -> bool

2. Adapter for working with references
  - fn as_ref<'r>(&'r self) -> Option<&'r T>
  - fn as_mut<'r>(&'r mut self) -> Option<&'r mut T>

3. Methods for getting to the contained value
  - fn expect(self, msg: &str) -> T
  - fn unwrap(self) -> T
  - fn unwrap_or(self, def: T) -> T
  - fn unwrap_or_else(self, f: &fn() -> T) -> T

4. Methods for transforming the contained value
  - fn map<U>(self, f: &fn(T) -> U) -> Option<U>
  - fn map_default<U>(self, def: U, f: &fn(T) -> U) -> U
  - fn mutate(&mut self, f: &fn(T) -> T) -> bool
  - fn mutate_default(&mut self, def: T, f: &fn(T) -> T) -> bool

5. Iterator constructors
  - fn iter<'r>(&'r self) -> OptionIterator<&'r T>
  - fn mut_iter<'r>(&'r mut self) -> OptionIterator<&'r mut T>
  - fn move_iter(self) -> OptionIterator<T>

6. Boolean-like operations on the values, eager and lazy
  - fn and<U>(self, optb: Option<U>) -> Option<U>
  - fn and_then<U>(self, f: &fn(T) -> Option<U>) -> Option<U>
  - fn or(self, optb: Option<T>) -> Option<T>
  - fn or_else(self, f: &fn() -> Option<T>) -> Option<T>

7. Other useful methods
  - fn take(&mut self) -> Option<T>
  - fn filtered(self, f: &fn(t: &T) -> bool) -> Option<T>
  - fn while_some(self, blk: &fn(v: T) -> Option<T>)

8. Common special cases that are shorthand for chaining two other methods together.
  - fn take_unwrap(&mut self) -> T
  - fn get_ref<'a>(&'a self) -> &'a T
  - fn get_mut_ref<'a>(&'a mut self) -> &'a mut T

Based on this, I have a few areas of the API of both modules to discuss:

# Renaming `unwrap` to `get`

This is a known issue (https://github.com/mozilla/rust/issues/9784), and I
think we should just go through with it.
There are a few things that speak in favor for this:
- The name `unwrap` implies destruction of the original `Option`, which is
  not the case for implicitly copyable types, so it's a bad name.
- Ever since we got the reference adapters, Options primary
  usage is to take self by value, with `unwrap` being the main method for
  getting the value out of an Option.
`get` is a shorter name than `unwrap`, so it would make using Option less painful. - `Option` already has two shorthands for `.as_ref().unwrap()` and `as_mut().unwrap()`: `.get_ref()` and `.get_mut_ref`, so the name `get` has precedence in the current API.

# Renaming `map_default` and `mutate_default` to `map_or` and `mutate_or_set`

I can't find an issue for this, but I remember there being an informal agreement to
not use the `_default` prefix in methods unless they are related to the
`std::default::Default` trait, a confirmation of this would be nice.

The names `map_or` and `mutate_or_set` would fit in the current naming scheme.

# The problem with Result

Now, the big issue. Up until now, work on the result module tried to converge on
option's API, except adapted to work with Result.

For example, option has `fn or_else(self, f: &fn() -> Option<T>) -> Option<T>`
to evaluate a function in case of a `None` value, while result has
`pub fn or_else<F>(self, op: &fn(E) -> Result<T, F>) -> Result<T, F>`
to evaluate a function in case of an `Err` value.

However, while some of the operations are directly compatible with this approach, most others require two methods each, one for the `Ok` and one for the `Err` variant:

- `fn unwrap(self) -> T` vs
  `fn unwrap_err(self) -> E`
- `fn expect(self, &str) -> T` vs
  `fn expect_err(self, &str) -> E`
- `fn map_default<U>(self, def: U, op: &fn(T) -> U) -> U` vs
  `fn map_err_default<F>(self, def: F, op: &fn(E) -> F) -> F`
- ... and many other methods in blocks 3, 4 and 5.

As you can see, this leads to API duplication twofold: All those methods
already exist on Option, and all those methods exist both for the `Ok` and the `Err`
variant.

This is not an Result-only issue: Every enum that is laid out like Result either
suffers the same kind of method duplication, or simply does not provide any,
instead requiring the user to match and manipulate them manually.

Examples would be `std::either::Either`, or `std::unstable::sync::UnsafeArcUnwrap`

To solve this problem, I'm proposing a convention for all enums that consist of
only newtype-like variants:

# Variant adapters for newtype variant enums

Basically, we should start the convention that every enum of the form

enum Foo<A, B, C, ...> {
    VariantA(A),
    VariantB(B),
    VariantC(C),
    ...
}

should implement two sets of methods:

1. Reference adapters for the type itself:
  - fn as_ref<'r>(&'r self) -> Foo<&'r A, &'r B, &'r C, ...>
- fn as_mut<'r>(&'r mut self) -> Foo<&'r mut A, &'r mut B, &'r mut C, ...>
2. Option adapters for each variant:
  - fn variant_a(self) -> Option<A>
  - fn variant_b(self) -> Option<B>
  - fn variant_c(self) -> Option<C>
  - ...

This would enable users to compose together any combination of any variant using the option API, while still having full control over ownership with the by-ref adapters.

# Example

If we combine the proposal above with the two renamings, we get this:

- `res.unwrap_err()` becomes
  `res.err().get()`
- `res.expect("!")` becomes
  `res.ok().expect("!")`
- `res.expect_err("!")` becomes
  `res.err().expect("!")`
- `res.map_err_default(0, |a| a + 5)` becomes
  `res.err().map_or(0, |a| a + 5)`

As you can see, it becomes a bit more cumbersome to use, but not necessarily longer.
We can also do the same for `Result` as we do for `Option`: Keep frequently
used methods as a shortcut, even if they are expressible by combining a few other.

# Possible problems for Result specifically

There are two potential issues that get in the way with this approach:
- Because `ok()` throws away the `Err` variant, something like `unwrap()` can no longer print a useful error message. However, the shortcut implementations still could, and as they will probably be used the most this might not be a problem in practice. - There was talk about making `Result` use an `~Error` trait object instead of a
  generic type `E`, which could invalidate most of this email.
However, this could also just mean that you usually will see `Result<T, ~Error>`
  in the wild, for which this proposal still applies.
  Additionally, even if the Pattern becomes useless for Result, the problem
still exists for any other newtype variant enums, so I'd like to see it get used
  anyway.

# Support for this pattern

Both reference adapters and variant Option adapters are fairly mechanically
implementations, so we could actually support their generation with an attribute
like `#[impl_enum_variant_accessors]`.

So, what are your thoughts about this? I think it would greatly increase composability of
enums in general and reduce the amount of boilerplate code. :)

Kimundi



_______________________________________________
Rust-dev mailing list
[email protected]
https://mail.mozilla.org/listinfo/rust-dev

Reply via email to