Why does `Option<T>` not implement the `Add` trait?

Option<T> implements the Sum trait, allowing to write [Some(1), Some(2)].into_iter().sum(). For a small, constant number of elements, this seems roundabout. However, the more direct Some(1) + Some(2) doesn't work because the Add trait is not implemented for Option<T>. I have a hunch this is not an oversight but a conscious decision. What are the problems with (for example) impl<T: Add> Add for Option<T>?

It's not clear what Some(3) + None should be. It depends on the use case. If None acts like 0, then you should get Some(3), but if None represents an error, you should get None instead.

The implementation of Sum for Option is in line with a handful of other implementations of iterator-consuming operations for fallible types.

These implementations only have the goal of making fallible operations possible to work with in iterators. Among the most often use of these is probably the support to use .collect() on an iterator of Result types:

(with unwrap, normal .collect() turning Iterator<Item = T> into Vec<T>)

use std::fs::File;

fn open_files(names: &[&str]) -> Vec<File> {
    names.iter().map(|name| File::open(name).unwrap()).collect()
}

(with propagation of error through Result; powered by .collect()’s support for directly turning Iterator<Item = Result<T, E>> into Result<Vec<T>, E> instead of Vec<Result<T, E>>; also properly short-circuiting)

use std::fs::File;
use std::io;

fn open_files(names: &[&str]) -> Result<Vec<File>, io::Error> {
    names.iter().map(|name| File::open(name)).collect()
}

These help deal with the limitation that, naturally, inside of closures passed to map or similar Iterator API, you can’t propagate errors (e.g. via ? operator) directly to an early-return from the containing function — it’s also a bit tricky to first learn for newcomers [it can be a bit hard to discover this].

And just like .collect() supporting this the FromIterator implementation for Result, .add() and .sum() work, too, via the respective Sum impl and Product impl.

By the way, all of these are implemented using a helper method not unlike itertools::process_results, which adapts an iterator-of-Result<T> as an iterator-of-T for a callback, but short-circuits [ending the callback by ending the iterator and discarding the result of the callback] whenever a Err is found after all.

Finally, Option – for being a type comparable to Result when you don’t want any explicit error values – supports these same things, too; probably in line with how Option has support for ? like Result and a generally similar API.


Now to back your question: Why no Add for Option? I have some thoughts on this.

First, there’s no need. The FromIterator/Sum/Product implementation supports specific iterator-related problems, solving them without the need to refer back to conceptually more tricky (and non-std) APIs like itertools::process_results, or even worse, the need to hand-roll solutions.

Admitted, std’s support for Result in iterators isn’t perfect either; itertools has many additional …_ok-postfixed helper methods to make fallible iteration even more convenient; also, for Add/Sum, one could probably also use something like .try_fold(0, |a, b| Ok(a + b?)) as a workable solution.

But still, I think it’s a very useful feature for iterators. On the other hand, if you just want to add two things in situation where creating either is fallible, you can use the ? operator without much of a problem.

And second, the current Sum implementations of Add-able types do just consume the whole iterator and fold them with +. However, if Option supported +, too, that would no longer be the case: The Sum and Product implementation for Option/Result are short-circuiting, which means that with Add<Option<T>> for Option<T> added, the operations iter_of_options.sum() and iter_of_options.fold(Some(0), |a, b| a + b) would behave significantly differently.

Now, this needn’t be too bad; and doing .fold(…, |a, b| a + b) manually is not very nice code anyways, but still…

Third, Options and Results are simply not number types. They generally are supposed to be handled manually and deliberately as soon as they appear.

For contrast, looking at the other existing Add implementations… e.g. we do find also &T supported for most types. However, the difference between &i32 and i32 will be insignificant in Rust, and those Add implementations exist to gloss over such differences a little bit more[1]; but doing this for Option and Result, too, might likely go a bit too far.[2] That being said, I don’t think it’s a particularly bad idea or anything like that. Ultimately I guess just: there are different trade-offs, less need for it compared to the Sum/Product(/FromIterator) support and thus we rather don’t add API we don’t really need, so we don’t have to live with it forever if it turns out to cause more problems than anticipated.[3]


Edit: Another thing that comes to mind thinking about a potential Add for Option and Result is the handling of different error types for Result. This does not apply to Option, but as mentioned above, Option and Result are generally trying to keep their API as similar as possible.

One could either support only Result<T, E> + Result<T, E> if the error types match, or one could support Result<T, E1> + Result<T, E2> = Result<T, E3> as long as E3: From<E1> + From<E2>. The former is somewhat restrictive, the latter is very prone to errors about ambiguous types – either way you’d end up with situations where (a? + b?) works but (a + b)? doesn’t; AFAIK the ambiguity issues are also a huge design issue that makes try { … } blocks difficult to get right. With try blocks, the syntax for addition of fallible arguments without propagating all the way out to the surrounding function looks like try { a? + b? }, and the same questions arise.

For now, using ? operator manually in the cases where it already works mostly avoids any issues – it supports differences between the error types, and the direct return to surrounding function can use that function’s explicit type signature to avoid ambiguities.


  1. of course, even with those, this support is only fairly rudimentary – it’s manually added for each numeric type; furthermore, for other operators like == the standard library has stuck the choice of generally not supporting &T == T or T == &T comparisons for most types, including numbers. Relevant design trade-offs here include: type inference becomes worse with such heterogeneous impls; heterogeneous &T == T or T == &T would require many specific impls for specific types, with Add and number types that’s already a lot of manual or macro-generated impls, but PartialEq is supported by a lot more types

    in any case… this support of glossing over T vs &T really only exists because Rust’s method calls syntax also supports glossing over &T vs T; the idea that we shouldn’t be more strict than necessary about handling of &T vs T is a general language design idea in Rust

    also, one possible takeaway from this comparison would be that with Option<T> + Option<T> support, additionally operations of the pattern Option<T> + T and T + Option<T> might become desirable; but this would be hard to do generically, and overlap errors would likely occur as soon as you’d also try to support this for heterogeneous Add implementations ↩︎

  2. Again, comparing to method calls as in the previous footnote about &T, you’ll see that for Option/Result those are not implicitly handled, either. You need to at least add something like ? there to call a method of T on Result<T>. And then that approach of adding ? also has different behavior! It will result in propagation of the error in the calling context – whereas an Add implementation could just push the None or Err out one single level. ↩︎

  3. You can’t exactly remove any trait implementations in Rust under the current stability guarantees. ↩︎

7 Likes

The implementations of sum and product for iterators have very natural semantics: perform some fallible operation on each element, and fold the successful results using add/mul. That's a relatively common, or at least reasonable operation on iterators, in line with methods like collect or filter_map.

Why would you want to introduce addition on Option<u32>? What is the problem where this operation is common?

1 Like

Thinking on what the others have already said I don't really see a reason on why that would be necessary, and on top of that @marcianx raised a good point too. It would be a convenient sugar to be able to just do Some(1) + Some(3) = Some(4), if a niche one, but then a decision would need to be made about what to do with None values. Still, I tried writing what I think would be the next best thing, but even then I had to cheese it a litle bit with the Default trait, and it is still kinda unecessary sugar in my opinion:

  1 use std::ops::Add;
  2 trait AddOpt<T> {
  3     type Output;
  4
  5     fn add(self, other: Self) -> Self::Output;
  6     fn add_strict(self, other: Self) -> Self::Output;
  7 }
  8
  9 impl<T: Add + Default> AddOpt<T> for Option<T> {
 10     type Output = Option<T::Output>;
 11
 12     fn add(self, other: Self) -> Self::Output {
 13         self.or(Some(T::default()))
 14             .map(|lhs| lhs + other.unwrap_or(T::default()))
 15     }
 16
 17     fn add_strict(self, other: Self) -> Self::Output {
 18         self.and_then(|lhs| other.map(|rhs| lhs + rhs))
 19     }
 20 }
 21
 22 fn main() {
 23     assert_eq!(Some(3), Some(2).add(Some(1)));
 24     assert_eq!(Some(2), Some(2).add(None));
 25     assert_eq!(None, Some(2).add_strict(None));
 26 }

Thank you very much for that detailed exposé of the various considerations and contexts at play. Indeed, with both iterators and the desired API similarity between Option and Result in mind, the perceived discrepency starts making sense, where it seemed puzzling in isolation.

I'd like to offer slight resistance against the first argument, “there's no need” for some certain API. In my opinion, one of the main reasons Options and Results are nice to deal with is their ergonomic API. They could be useful without is_some_and() or map_or_else(), but to really “feel nice,” they “need” these methods. I'm aware this is both wishy-washy and subjective, and I understand what you mean in this context, hence why this is only slight resistance. :slightly_smiling_face:

I must admit that I find the third argument, Options and Results not being number types, not overly compelling given that both do implement Sum. In fact, looking at the implementors of Sum, Option and Result stand out as being the only two non-number, non-duration types. An argument along the lines of “Sum is implemented for many types, but Add only for number types” doesn't seem to hold up. Indeed, the Add trait is also implemented for a few non-number, non-duration types, like String and Assume, a type I hadn't known existed but seems to be non-numerical in nature.

Despite what my lengthy reply here might suggest, I'm not trying to argue in favor of adding an implementation of the Add trait for Option. Thanks again for pointing out the many design considerations that are at play, as well as the practical downsides of having such an implementation.

3 Likes

I don't want to introduce anything; I want to understand why things are the way they are. In my experience, the APIs of the types in the Rust standard library are very well thought out. In isolation, seeing an implementation of Sum but not Add was puzzling to me, and I was looking for a solution to that puzzle.

I apologize for apparently not having been overly clear in the OP. I'm not trying to suggest that adding an implemantion of Add for Option would be necessary or beneficial.

If such an implementation were to be added though, I'd be surprised if it behaved differently from the implementation of Sum. I would expect something like your add_strict(), e.g.:

impl<T, U> Add<Option<U>> for Option<T>
where
    T: Add<U>,
{
    type Output = Option<<T as Add<U>>::Output>;

    fn add(self, rhs: Option<U>) -> Self::Output {
        Some(self? + rhs?)
    }
}

I see the non-obvious behavior of Add for Option as a clear indicator that this would be a questionable addition at best.

2 Likes

Generally, Rust is designed according to the principle of "don't try to guess the future". The APIs are kept minimal, and not introduced just because we can or because there is a similar API somewhere. There must be specific use cases, and APIs that don't pull their weight by making someone's life easier don't get stabilized.

In other words, a very valid reason why some API is missing is "nobody has put in the work to add it", either because no one had a good use case, or just because no one though about that specific addition. So that's the first question: why do you think that impl would be useful? If you can't think of a reason, then that's a sufficient answer on its own. Other people probably didn't think of a reason either. That doesn't mean that there are any technical blockers, or that the API would be rejected if you provided a reasonable use case.

5 Likes

According to category theory, Options and Results are sum types so they should implement Sum! (joking)

This topic was automatically closed 90 days after the last reply. We invite you to open a new topic if you have further questions or comments.