Have function return different result type when argument is omitted

How do we go about this without creating a duplicate function Y?

impl X {

 fn Y (arg:impl Into <Option<u64>>) -> Result<T, Err>{
      // returns different result T if arg is missing.
  }

}

My first idea would be to wrap Result T into an Option or Enum?

A simple way, often used in Rust, is to just have y() and y_with_arg(arg) functions. To avoid code duplication, one can call another, or both can use a third helper function.

To really vary output type based on the type of the input argument, you would need tricks with generics, like so:

fn y<T>(arg: T) -> <T as ACleverTrait>::Output where T: ACleverTrait
impl ACleverTrait for u64 {
   type Output = Result<u64, IntErr>
}

impl ACleverTrait for () {
   type Output = ()
}

which as you can see, takes complexity to another level, and you will need even more to actually do useful things with the argument type.

3 Likes

For perhaps more context,

  • Rust is statically typed
    • at an actual call site, you can't return more than one type
    • an actual function (with all generic parameters resolved) can't take both 0 and 1 arguments[1]
  • Rust enum variants have the same type as their containing enum
    • so Some::<T> and None::<T> both have type Option::<T>

Keeping this in mind while looking at your OP code, we find that

  • arg can't be missing, you'll always have some value
    • But perhaps you just mean arg.into() is None
  • You can't return a different type based on if arg.into() is Some(_) or None
    • But perhaps you just want some return value distinguishable by the caller

If you really want different types, that's what @kornel's answer is about.

But if we're just talking about values, wrapping the result type in an Option is one way to go about it:

/// # Returns
/// - `None` if and only if `arg.into()` is `None`
/// - `Some(Err(_))` if blah blah blah
/// - `Some(Ok(_))` otherwise
fn y(arg: impl Into<Option<u64>>) -> Option<Result<T, Err>> {
    let arg /* : u64 */ = arg.into()?;
    let result /* : Result<T, Err> */ = todo!();
    Some(result)
}

If you control Err, adding some variant/flag for "missing input" or whatever is another possibility:

fn y(arg: impl Into<Option<u64>>) -> Result<T, Err> {
    let arg /* : u64 */ = arg.into().ok_or(Err::MissingInput)?;
    todo!()
}

But honestly, if the impl Into<Option<u64>> is just there so people can go

y(0);
y(42);
y(None); // <-- always gives the same trivial answer

My actual advice is "don't do that", and just use arg: u64.

(If users of y have opt: Option<u64>, they can do let _ = opt.map(y) on their end.)


  1. you can sort-of do this on nightly with a custom struct (but functions still don't do this) ↩ī¸Ž

4 Likes

but how do I tell my user that the argument is optional? Should it simply be written in the documentation?
What are the advantages of just going u64?

What should happen when the argument is not provided? Not "what value should the function return?", but "what change will it make to the environment?" If the answer is "nothing" - then you don't have to tell this, they will find out everything they need purely from types. If there's some side effect - it obviously have to be documented.

1 Like

From what I had to go on in your OP...

...I surmised that the None case wouldn't actually do anything, and just immediately return None if the user passed in None...

fn meaningful_y(arg: u64) -> Result<T, Err> {
    // Actually do stuff
}
fn y(arg: impl Into<Option<u64>>) -> Option<Result<T, Err>> {
    arg.into().map(meanigful_y)
}

...in which case the question is, what's the advantage of not just going u64? Calling it with None is a no-op, and the consumer of this API can use map and the like on their own Option values if they need to; that's why those methods are on Option.

(To answer your question though, some advantages of just u64 include: (1) not monomorphizing unnecessarily (2) not doing more work unnecessarily in the presumably common case (3) being idiomatic)


If what I surmised was wrong and you still do something meaningful in the None case, that's different.

2 Likes

In my use case this function would send an API get request where the endpoint allows one parameter to be left out, in which case the API response is either a Vec of Struct or a single Struct.

Therefore I either have two functions where one sends a parameter and the other doesn't, or I use Option to check internally that None was provided so the API call doesn't send any parameter.

Therefore I check internally

if arg.into().is_some()
else // make call without param

I've seen this pattern in some crates and thought it was idiomatic enough?

Sounds like the "actually talking about different types" scenario, and so this reply is more applicable than my replies.

I meant it's unidiomatic to force callers to provide an arg: Option<X> if all you do is arg.map(|x| the_actual_work). You're not, so...

1 Like

Thank you, the above solution uses advanced Type features of Rust and I am not familiar with those yet. for where as T these keywords seem a bit strange for me now. Why can he declare a Type inside Impl? What is the reason anyways?

You can just use the y and y_with_arg approach.

It's part of the definition of a trait -- an associated type. Here's a more complete example. And one reason is to support things like returning different types in a statically typed language.

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.