Idiomatic way to set string default value to unwrap

Hello all!

I was trying to capture the host and port settings for actix.

I have the following line:

fn main() -> std::io::Result<()> {
    let host = var("HOST").unwrap_or_else(|_| "127.0.0.1".to_string());
    let port = var("PORT").unwrap_or_else(|_| "8080".to_string()).parse::<u16>().unwrap();

   ...
}

(I am also quite baffled by why a str could not be copied and a String must be used, but I digress.)

I was using unwrap_or("127.0.0.1".to_string()) earlier but Clippy complains about or_fun_call.

Would this be a false positive on clippy’s part or is there any deeper reason for using a closure?

What is the idiomatic way to set default value for unwrap that is different than that of unwrap_or_default()?

.unwrap_or("127.0.0.1”.to_string()) always allocates new string even if it’s not needed as self is Some(_), while .unwrap_or_else() only call the closure when it’s needed.

It seems you want to create SocketAddr based on env vars, so you wouldn’t actually need a default String. And also with your PORT handling logic, this code will crash instead of falling back to default if the PORT exist but invalid number like “foobar”. To handle this Option::and_then is your friend.

let host: IpAddr = var("HOST").and_then(|host| host.parse().ok()).unwrap_or("127.0.0.1".parse().unwrap());
let port: u16 = var("PORT").and_then(|port| port.parse().ok()).unwrap_or(8080);
1 Like

|x| x.parse().ok() returns a Option, which makes and _then complain about that.

map_or_else seems to be an option but it is an experimental API.

Oh, I thought that env::var returns Option, but actually it returns Result. :stuck_out_tongue:

If you don’t care the reason why env::var or str::parse failed like this case, you can call var("Host").ok() to make it Option and drop possible error description.

1 Like

Thanks!

Also, I’ve discovered IpAddr::V4 and IpAddrV4::new. So no parsing is needed for the default value!

    let host: IpAddr = var("HOST").ok().and_then(|host| host.parse().ok())
        .unwrap_or_else(|| IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)));

Note that unlike String, Ipv4Addr is cheap to construct, so here it would be more idiomatic to use plain .unwrap_or():

const LOCALHOST: IpAddr = IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1));
let host = var("HOST")
    .ok()
    .and_then(|host| host.parse().ok())
    .unwrap_or(LOCALHOST);
3 Likes

That’s all about memory management, which in Rust is done via ownership.

var("HOST") gives you an owned object (it’s a string of any length, obtained at run time, so there’s no good place to put it other than the heap), and it has to be dropped later. The compiler will insert drop(var) at the end of the function.

However, if var contained "127.0.0.1" str, then dropping of it would be illegal (you can’t free a literal built-in into the executable — it’s not on the same heap as String data).

So there are two ways to solve it:

  • There would need to be a flag that remembers whether host was from the env var (owned), or from the fallback (borrowed). The Cow type holds exactly such flag, so if you wrapped both in Cow, it’d work.

  • You could store host in its own variable, and then use borrowed value. The compiler would track dropping of the variable itself. And if all later usage was borrowed, it’d be safe to mix borrow of a variable with borrow of the literal.

// owned type, the compiler will drop
let host = var("HOST"); 
// borrowed types, don't need dropping
let host = host.as_ref().unwrap_or_else(|_| "127.0.0.1"); 

This separation into two let statements is essential, because let makes the compiler extend the lifetime to the scope of let rather than just the single temporary expression.

2 Likes

You might also like Ipv4Addr::LOCALHOST

2 Likes