I'm gonna be honest: I never liked {Option,Result}::transpose
.
I mean, on the one hand, it's one of those tools that you can't possibly use incorrectly (due to the type system), but on the other hand:
- I don't like the fact that they have the same name. When I see a call to
transpose()
, it's not easy to tell if it's converting fromOption<Result<T>>
toResult<Option<T>>
or the other way around. - I find it conceptually hard to deal with, because it takes two layers of an abstraction (both closely associated with control flow) and pulls them inside-out. I feel this obscures the flow of control versus simply constructing the right type to begin with.
- As a personal issue, I write a lot of numerical code that deals with matrices. Having a call to
.transpose()
in such code could look extremely misleading, so I wrote an extension trait to rename it tofold_ok()
.
But even more notably: Today, I had just written some code that used transpose
when it suddenly dawned on me what the abstraction is that I was really looking for:
impl<T> Option<T> {
fn try_map<B, E>(
self,
func: impl FnOnce(T) -> Result<B, E>,
) -> Result<Option<B>, E> {
self.map(func).transpose()
}
}
I searched my codebase, and sure enough, every single call to transpose
(fold_ok
in the bulk of my codebase) was immediately preceded by a call to map
:
opt.map(|s| {
let tokens = lex(s.span(), &format!("where {}", &s[..]))?;
Ok(syn::parse2(tokens)?)
}).transpose()
// --------
*bonds = settings.bond_radius.map(|bond_radius| FailOk({
Rc::new(FracBonds::compute(&original_coords, bond_radius)?)
})).fold_ok()?
// --------
nonempty_var("RSP2_LOG_MOD")?
.map(|s| match &s[..] {
"1" => Ok(true),
"0" => Ok(false),
_ => bail!("Invalid setting for RSP2_LOG_MOD: {:?}", s),
}).fold_ok()?
.unwrap_or(false)
// --------
nonempty_var(MAX_OMP_NUM_THREADS)?
.map(|s| s.parse())
.fold_ok()?
.unwrap_or(::num_cpus::get() as u32)
Isn't try_map
the method that we really want?