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, Option
s and Result
s 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.
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
orT == &T
comparisons for most types, including numbers. Relevant design trade-offs here include: type inference becomes worse with such heterogeneous impls; heterogeneous&T == T
orT == &T
would require many specificimpls
for specific types, withAdd
and number types that’s already a lot of manual or macro-generated impls, butPartialEq
is supported by a lot more typesin any case… this support of glossing over
T
vs&T
really only exists because Rust’s method calls syntax also supports glossing over&T
vsT
; the idea that we shouldn’t be more strict than necessary about handling of&T
vsT
is a general language design idea in Rustalso, one possible takeaway from this comparison would be that with
Option<T> + Option<T>
support, additionally operations of the patternOption<T> + T
andT + 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 heterogeneousAdd
implementations ↩︎Again, comparing to method calls as in the previous footnote about
&T
, you’ll see that forOption
/Result
those are not implicitly handled, either. You need to at least add something like?
there to call a method ofT
onResult<T>
. And then that approach of adding?
also has different behavior! It will result in propagation of the error in the calling context – whereas anAdd
implementation could just push theNone
orErr
out one single level. ↩︎You can’t exactly remove any trait implementations in Rust under the current stability guarantees. ↩︎
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?
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 Option
s and Result
s 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.
I must admit that I find the third argument, Option
s and Result
s 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.
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.
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.
According to category theory, Option
s and Result
s 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.