How to use a default value for an optional function parameter that is defined as generic?

My function shall be able to be called with a path and file name as optional parameter. But if this is None, I'd like to choose a default value. To keep the function signature convenient for usage it is defined as:

pub fn loadEnv<F: AsRef<Path>>(file_name: Option<F>) -> Result<()> {}

So it can be called by passing a &str:

loadEnv(Some("./dummy-env.json")).

However, unwrapping the option and providing a default value fails as the default does not math the generic type:

pub fn loadEnv<F: AsRef<Path>>(file_name: Option<F>) -> Result<()> {
    let file_name = file_name.unwrap_or(Path::new("default-env.json"));
mismatched types
expected type parameter `F`
        found reference `&std::path::Path`rustcE0308

While I know why this does not compile I've no clue whether the is a solution to it, keeping the generic parameter. Changing the signature to accept Path as type would fix it but loose the flexibility to be called with a simple &str .

Any hint is very welcome.

1 Like

Of course, because all your function knows is that F: AsRef<Path>, not that F is a &Path. (And for all you know, it might not be: that's the entire point of making the function generic.) Consequently, you have to actually convert the parameter to a &Path before you can treat it as such:

let file_name = file_name.as_ref().map_or(
    Path::new("default-env.json"),
    AsRef::as_ref,
);
3 Likes

While @H2CO3 already provided a short solution involving some API from the Option type, it’s important to know that Option is just an enum type with the two variants Some(value) or None. The way of handling enums that always gets the job done is: use match.

The other thing to note here is that you’re taking <F: AsRef<Path>>, probably with the goal of producing a &Path eventually. Your previous approach of first converting Option<F> into F and then later probably &F into &Path cannot possibly work, because you can in fact not create a value of type F in case the option is None.

But the solution isn’t too hard. If your Option<F> is Some(value) then you have a value: F inside and can use AsRef to get the str, otherwise you can use your default &Path.

The first/easiest approach with match that might work is to just split up the whole function

match file_name {
    Some(value) => {
        let file_name_path = value.as_ref();
        // use file_name_path
    }
    None => {
        let file_name_path = Path::new("default-env.json");
        // use file_name_path
    }
}

Factoring out the // use file_name_path so that it’s not duplicated isn’t 100% trivial, e.g. a first approach might be

let file_name_path;
match file_name {
    Some(value) => {
        file_name_path = value.as_ref();
    }
    None => {
        file_name_path = Path::new("default-env.json");
    }
}
// use file_name_path

which doesn’t work, or

let file_name_path = match file_name {
    Some(value) => {
        value.as_ref()
    }
    None => {
        Path::new("default-env.json")
    }
};
// use file_name_path

which doesn’t work either. Both can be fixed by also moving value out of the scope of the match, e.g. like this

let value_in_outer_scope;
let file_name_path = match file_name {
    Some(value) => {
        value_in_outer_scope = value;
        value_in_outer_scope.as_ref()
    }
    None => {
        Path::new("default-env.json")
    }
};
// use file_name_path

but it isn’t very pretty…

Also, this match does move the Option<F>, and thinking about it, you actually only need to borrow it, which leads to the solution that @Hyeonu already suggested below:

let file_name_path = match &file_name {
    Some(value) => {
        // value: &F
        value.as_ref();
    }
    None => {
        Path::new("default-env.json");
    }
};
// use file_name_path

This approach would’ve also worked in the other version:

let file_name_path;
match &file_name {
    Some(value) => {
        file_name_path = value.as_ref();
    }
    None => {
        file_name_path = Path::new("default-env.json");
    }
};
// use file_name_path

The combinators in the solution by @H2CO3 work as follows:

It also borrows the option, so it starts with an &Option<F>, which can be turned into Option<&F> with Option::as_ref.(i.e. the file_name.as_ref() call). Then, the by-value combinators of Option can be used.

As you noticed, unwrap_or isn’t applicable yet since we’re unable to provide a value of type F (or rather &F now…), but map helps. You can do file_name.as_ref().map(F::as_ref) to get an Option<&Path>. (file_name.as_ref().map(F::as_ref) is the same as file_name.as_ref().map(|value| value.as_ref()).

Then finally on the Option<&Path>, you can use unwrap_or, so there’s the solution:

let file_name_path = file_name
    .as_ref()
    .map(F::as_ref)
    .unwrap_or(Path::new("default-env.json"));
// use file_name_path

Finally, F::as_ref can also be written AsRef::as_ref (both are shorthands for <F as AsRef>::as_ref, and type inference can fill in the missing parts), and .map_or(default, function) is a short-hand for / combination of .map(function).unwrap_or(default).

3 Likes

I also think plain'ol match would be better for this case.

let file_name = match &file_name {
    Some(name) => name.as_ref(),
    None => Path::new("default-env.json"),
};
1 Like