How to reduce the indentation when using option combinators?


#1

I have a few functions in a project that use options and the .and_then() combinator; I don’t like the excessive indentation it causes—I find that it makes the code harder to read. I would prefer if the code was nicely left-aligned as if I was using the ? operator or .unwrap().

fn eval_setop(op: SetOp, left: &FFBase, right: &FFBase, env: &Env, db: &EnvDb, im: &InternmentMap) -> Option<bool> {
    use self::FFBase::*;

    match (op, *left, *right) {
        (SetOp::OneOf, Var(im_id), IntList(il_id)) =>
            im.extract(env, im_id).and_then(|cp_id| {
                db.ilcp.value_of(cp_id).and_then(|left_list| {
                    db.ilcp.value_of(il_id).and_then(|right_list| {
                        Some(left_list.iter().any( |x| right_list.binary_search(x).is_ok() ))
                    })
                })
            }),
       // elided

#2

I don’t think there is a standard language solution to this.

Specifically for and_then (which is where this seems to be the greatest problem), there’s an RFC I was rooting for which would help alleviate this, but it was postponed.

let Some(cp_id) = im.extract(env, im_id) else { return None; }
let Some(left_list) = db.ilcp.value_of(cp_id) else { return None; }
let Some(right_list) = db.ilcp.value_of(il_id) else { return None; }

I am uncertain whether there are plans to support use of the ? operator on Options, but it would certainly make a lot of people’s lives easier:

let cp_id = im.extract(env, im_id)?;
let left_list = db.ilcp.value_of(cp_id)?;
let right_list = db.ilcp.value_of(il_id)?;

Inspired by the old try! form of ?, you can write your own try_some macro. Here’s something I used to use in personal projects, which supported any form of diverging expression: (for the purposes of sharing, I simplified it just now in a manner which unintentionally supports non-diverging expressions; I wouldn’t write one, but YMMV)

macro_rules! try_some {
    ($expr:expr)
    => { try_some!($expr, else return ::std::option::Option::None) };

    ($expr:expr, else $($fail:tt)+)
    => { match $expr {
        ::std::option::Option::Some(val) => val,
        ::std::option::Option::None => $($fail)+,
    }};
}


let cp_id = try_some!(im.extract(env, im_id));
let left_list = try_some!(db.ilcp.value_of(cp_id), else panic!("at the disco"));
let right_list = try_some!(db.ilcp.value_of(il_id), else continue);

Haskell, a language which fundamentally relies on combinators like this, has do notation to reduce nesting. I don’t suspect a Rust equivalent is coming any time within the next century.

In summary: _/o\_


#3

Thanks for the detailed response; I think I’ll leave the code as-is for now, work on another part of the project, and see later if ? has been implemented for options or another alternative exists.


#4

Maybe a helper function or two could simplify the code a bit? I guess that’s similar in spirit to @ExpHP’s suggestion of a helper macro, but something a bit higher level.


#5

I’m not sure what you have in mind for a function-based solution. It’d certainly be cleaner if possible, but the trouble with functions is that they can’t do control flow.

IMO, and_then is the helper function here, and its functional nature is unfortunately what makes it impossible to eliminate the rightward drift.


Edit: actually, I think I see. Normally, when and_then chains involve the same object, you can do

Dumb thoughts, please ignore
x.and_then(|v| ...)
    .and_then(|v| ...)
    .and_then(|v| ...)
    // ...

but the reason you can’t do that here is because you have three different options; so I guess you could write something with the signature:

fn and_then_thrice<A, B, C, R, F>
    (Option<A>, Option<B>, Option<C>, F)
    -> Option<R>
where F: FnOnce(A, B, C) -> Option<R>,

and if you wanted a solution for a general number of Options, you could probably implement an extension trait on tuples of options

trait AndThen {
    type Args;  // = (A, B, C)

    fn and_then<R, F>(self, F) -> Option<R>
    where F: FnOnce(Args) -> Option<R>;
}

impl<A,B> AndThen for (Option<A>, Option<B>) { ... }
impl<A,B,C> AndThen for (Option<A>, Option<B>, Option<C>) { ... }

Edit 2: never mind, that’s not true about your situation at all. I should think a bit more first, then respond :stuck_out_tongue:


#6

Note that the following has landed, so 1.22 should allow you to use ? with your Options:

https://github.com/rust-lang/rust/pull/42526

Edit: That will make @juggle-tux’s helper shorter:

fn option_pair<T, U>(a: Option<T>, b: Option<U>) -> Option<(T, U)> {
    Some((a?, b?))
}

#7

hmm i think a helper function like this would be useful here

fn option_pair<T, U>(opt_a: Option<T>, opt_b: Option<U>) -> Option<(T, U)> {
    match (opt_a, opt_b) {
        (Some(a), Some(b)) => Some((a, b)),
        _ => None,
    }
}

turning the and_then nesting into this

    match (op, *left, *right) {
        (SetOp::OneOf, Var(im_id), IntList(il_id)) => im.extract(env, im_id)
            .and_then(|cp_id| {
                option_pair(db.ilcp.value_of(cp_id), db.ilcp.value_of(il_id))
            })
            .and_then(|(left_list, right_list)| {
                Some(
                    left_list
                        .iter()
                        .any(|x| right_list.binary_search(x).is_ok()),
                )
            }),
    }

making me wonder why Option don’t has a a zip method like Iterator's


#8

What I had in mind was functions that return an Option<bool> but wrap some of this nestedness in them. So not so much helper functions dealing with Options themselves, but something higher level specific to the domain of the objects involved. The control flow here is messy because there’s a lot of “drill down” and destructuring going on manually, possibly violating Demeter’s Law. Hiding that behind domain/type specific methods may help.


#9

What I’ve done in the past is to convert the Options into Results and then back afterward. E.g.

fn eval_setop(op: SetOp, left: &FFBase, right: &FFBase, env: &Env, db: &EnvDb, im: &InternmentMap) -> Option<bool> {
    use self::FFBase::*;

    match (op, *left, *right) {
        (SetOp::OneOf, Var(im_id), IntList(il_id)) => (|| -> Result<_, _> {
            let cp_id = im.extract(env, im_id).ok_or(())?;
            let left_list = db.ilcp.value_of(cp_id).ok_or(())?;
            let right_list = db.ilcp.value_of(il_id).ok_or(())?;
            Ok(left_list.iter().any( |x| right_list.binary_search(x).is_ok() ))
        })().ok(),
       // elided

If you’re in control of most of these functions, you can make life a bit easier by tweaking the APIs to return Result<T, ()> instead of Option<T>.