API design: &Option<T> vs Option<&T>

When designing an API. Is it better for the input argument to the function to be:

&Option<T>

or

Option<&T>

?

Since the caller can convert from former to latter, but not the other way round, I believe Option<&T> is better, as it's more universal.

4 Likes

The latter is strictly more general because you can always go from &Option<T> to Option<&T> (with as_ref), but you can't go from Option<&T> to &Option<T>

4 Likes

Thanks. This all makes sense. It's a bit counter intuitive since:

  1. for args, we often do (e: Expr) -> (e: &Expr), but here are moving the & inside

  2. in general, using a ref as an arg to a generic seems like it might cause lifetime issues

Think of it this way, with &Option<T> you are forcing the user to have an Option somewhere, that way they can pass a reference to it. But with Option<&T> you are only forcing them to have a T somewhere (and that may be inside another Option, but it could also be somewhere else)

I prefer Option<&T> because it usually makes more sense.

For example, when I see &Option<T> it says "I'm giving you a pointer to something which may or may not exist", whereas Option<&T> says "something may or may not exist, and if so here's a pointer to it".

I don't normally see Option<&T> as an input parameter but it makes a lot of sense as a return value (e.g. find()), and you may want to stay consistent with that.

1 Like

I'm not sure if I expressed my view clearly:

  1. I agree that fn arguments, Option<&T> is better than &Option<T>

  2. By "counter intuitive" I am referring to the fact that most func arguments

use &Vec<T> more than Vec<&T>
use &Sst<T> more than Set<&T>
use &HashMap<K, V> more than HashMap<&K, &V>
use &Rec<T> more than Rec<&T>

Option seems to be the special case here where (1) the constructor is O(1) time and (2) some other property that I can't quite formalize yet

I don't think that's the case at all.

In Rust, every container type has a well-defined, and what's more important, unique purpose. Your API design should thus probably depend on the specific use case and the corresponding container type, not on some vaguely general rule. Don't try to compare API use of one container to API use of a completely different data structure.

Others have already explained why Option<&T> is more useful. I'd like to add that no function should take &Vec<T> explicitly because that requires a potential extra allocation when a slice (&[T]) would have sufficed. And a slice can already provide pointers to its elements. So that's another different API design viewpoint to take into consideration. A HashMap is, again, used differently, but it's rarely found in function argument position, because it's more often created by algorithms that perform a mapping, or used internally as part of a more complex data structure that requires mappings or uniquing or whatnot.

Conclusion: I'd be wary of any general rule about choosing a container type in function argument position, and I suggest you always keep in mind the specific problem you're trying to solve instead.

I hope I am not nitpicking / strawman-ing your argument here, but I completely disagree with this.

I find it very common to

  1. declare a struct like:
pub struct Foo {
  a: TypeExpr1,
  b: TypeExpr2,
  c: TypeExpr3,
}

where TypeExprN all do NOT contain references / lifetimes.

  1. Then, there are functions which ends up taking a foo: &Foo and passing sub parts of foo by reference, thus providing arguments of type &TypeExpr1, &TypeExpr2, &TypeExpr3

Assuming the above logic is correct, it is very normal to have & in the outer most position of a TypeExpr rather than inside it.

Ah, maybe my question title "API design: ..." is causing the confusion. It sounds like I'm asking about the public interface a crate provides, whereas I really concerned with all functions within the crate (even the private ones).

Yeah, private functions can go crazy however it's convenient for the crate author. After refactoring, I, too, end up with functions taking vectors and hash maps by value, because I don't need to care. If those functions were public, I would probably not take any collection at all; I'd make them accept an iterator instead.

2 Likes