Bool::then_or_default?

I recently discovered bool::then, and found it very useful for writing (in my opinion) clearer code that more concisely expresses my intent.

fn functional_string(condition: bool) -> Option<String> {
    condition.then(|| String::from("Functional!"))
}

fn imperative_string(condition: bool) -> Option<String> {
    if condition {
        Some(String::from("Imperative!"))
    } else {
        None
    }
}

In a way, bool::then is similar to Option::map.

fn functional_path(path: Option<&Path>) -> Option<std::io::Result<PathBuf>> {
	path.map(std::path::absolute)
}

fn imperative_path(path: Option<&Path>) -> Option<std::io::Result<PathBuf>> {
	if let Some(path) = path {
		Some(std::path::absolute(path))
	} else {
		None
	}
}

These two functions create a nice symmetry.


However, there is no equivalent for Option::unwrap_or_default. This is another useful function that I find myself reaching for quite often. You can, of course, call then followed by unwrap_or_default, or just handle it imperatively:

fn functional_vec(condition: bool) -> Vec<String> {
    condition.then(|| vec![String::from("Functional!")]).unwrap_or_default()
}

fn imperative_vec(condition: bool) -> Vec<String> {
    if condition {
        vec![String::from("Functional!")]
    } else {
        Vec::new()
    }
}

Ideally, I'd like for there to be something along the lines of bool::then_or_default, which would operate similarly to bool::then -> Option::unwrap_or_default.

fn hypothetical_vec(condition: bool) -> Vec<String> {
    condition.then_or_default(|| vec![String::from("Hypothetical!")])
}

The crate bool_ext implements this, and a lot more. They named their version bool::map_or_default.


I don't know how useful this would be, but there could also be something like a bool::if method that behaves vaguely like Option::map_or_else...

fn hypothetical_if(condition:bool) -> &'static str {
    condition.if(|| "True!", || "False!")
}

This is kind of like a functional version of a ternary statement. Not sure I like it, honestly.


I understand that this is a pretty niche use case, but in my opinion, bool::then_or_default would be nice to have in the situations it comes up.

Option has several other methods that might be worth mirroring for bool - a couple were mentioned on this IRLO post.

To formally propose this, file an ACP.

I disagree with the adjectives used in describing the status quo. if cond { … } else { … } in Rust already is very much “functional (programming) style”, not a statement but an expression, and hence not “imperative”.

In fact, speaking of actual functional programming languages like e.g. Haskell or Ocaml or such, I find their syntax of an if cond then foo else bar expression very much equivalent to Rust’s if cond { foo } else { bar }. Whereas in C you would need to spell the same kind of thing (aka a ternary expression) very differently, as cond ? foo : bar.


Maybe one main remaining difference here is the formatting? It can go all in one line of course

fn imperative_vec(condition: bool) -> Vec<String> {
    if condition { vec![String::from("Functional!")] } else { Vec::new() }
}

The other notable differences are: Vec::new() is explicit, and calling …::default on Vec or Default or so is more verbose than one would like. The desire to be able to call default() as a standalone function is coming up every so often.

If default() was callable directly… then between

if cond { case_1 } else { default() }
// and
cond.then_or_default(|| case_1)

there isn’t that much of a length difference left even… Note that .then is different in that it abbreviates away also any mention of Some and None:

if cond { Some(case_1) } else { None }
// vs
cond.then(|| case_1)

And the last significant difference is that if doesn’t work postfix at the end of a method chain…

foo
    .bar(… something something …)
    .baz(… something something …)
    .check_qux()
    .then(|| ………………………………
                    …………………………
2 Likes

That's fair. I only used "imperative" because I couldn't think of a more apt word to use. Perhaps I could have used "procedural", although that carries mostly the same implications as "imperative" does.

With regards to formatting, there's a minor nit to be aware of: rustfmt will, by default, wrap if statements more readily than it would, for example, a unwrap_or_else. This is controlled by the single_line_if_else_max_width parameter, and defaults to a percentage of your maximum line length. This means that, depending on how long/deeply nested it is, a statement like

if condition { vec![String::from("Functional!")] } else { Vec::new() }

could get automatically reformatted to

if condition { 
    vec![String::from("Functional!")]
} else {
    default()
}

in a situation where

condition.then_or_default(|| vec![String::from("Functional!")])

might not be.

4 Likes