Optional chaining

Is this one proper way to rewrite JavaScript's o?.f1()?.f2() in Rust, given o is an Option?

o.map(|o| o.f1().map(|o| o.f2()))

// or o.and_then(...)
  • The callback given to and_then must return an Option.
  • The callback given to map returns anything.

ChatGPT didn't tell at first

Try it yourself. You have the playground at your disposal.

I can save you some typing, but the code above will create an Option<Option<T>>. If you want to flatten the options, you have to use and_then instead in the outer-most call.

1 Like

Yes, I used the playground, but I was told they were equivalent

Oops, it was my wrong interpretation.

Map will do an operation on an Option<T> and return a new Option<U> with the result of that operation. If this operation itself returns an Option<S> then you have a nested Option<Option<S>> which is annoying. Option::and_then() can help with that by removing a layer of Option

fn main() {
    let a = Some(32).and_then(f1).map(f2);
    dbg!(a);   //    ^            ^map because f2 doesn't return an option.      
               //    ^and_then because f1 returns an option.
}

fn f1(x: i32) -> Option<i64> {
    Some((x * x) as i64)
}

fn f2(x: i64) -> String {
    format!("{x}")
}
Compiling playground v0.0.1 (/playground)
    Finished dev [unoptimized + debuginfo] target(s) in 0.39s
     Running `target/debug/playground`
[src/main.rs:3] a = Some(
    "1024",
)
1 Like

as @SebastianJL pointed above, you cannot chaining callbacks returning arbitrary types, you need to make them returning Option too.

JS is dynamically typed, and every value can be nullish, so the chaining works for anything.

rust, on the other hand, is statically typed, only Option<T> can be None, so chaining is only valid when all the intermediate value down the chain are all Options.


ps:

rust have a similar ?' syntax too, but, in JS, the chaining operator is ?.` (it's one single operator, note the dot there), and they work very differently:

in javascript, if what you are chaining is not a method call or property access, you must use these oddly looking syntax:

// array index
var x = arr?.[42];
var arr = null;
// function call
var fun = null; 
var x = fun?.();
// attributes, not lexical variable:
this.on_error?.()

in rust, the ? operator is just sugar syntax for a match expression, so it can be used anywhere an expression can be used, and it is not limited to Option, notably, Result can use the ? syntax too. (for technical details,
see the Try trait)

here's a trick if you want to use the ? syntax but the current function does not return Option, you can use a closure to set up a local type context for the ? operator. for instance, the example code snippet by @SebastianJL can be rewritten as:

    let x = Some(32);
    // explicit types
    let a: String = |x: Option<i32>| -> Option<String> {
        let x = x?;
        let x = f1(x)?;
        let x = f2(x);
        Some(x)
    }(x)
    .unwrap();
    dbg!(a);

    // alternative: use type inferrence, shorter
    let a = |x| -> Option<_> { Some(f2(f1(x?)?)) }(x).unwrap();
1 Like

I just realized that in the original example f1() and f2() aren't freestanding functions but methods of the return values. So the above examples would look more like this:

let a = o.and_then(|x| x.f1()).map(|x| x.f2()).unwrap()

or

let a = |x| -> Option<_> { Some(x?.f1()?.f2()) }(o).unwrap();

where o is the original option of an unknown type that has a method f1() returning an option of a type that has a method f2().

Here an actual example

fn main() {
    let o = Some("32");
    let a = o
        .and_then(|x| x.parse::<i32>().ok())
        .map(|x| x * x)
        .unwrap();
    dbg!(a);
}
    Finished dev [unoptimized + debuginfo] target(s) in 5.36s
     Running `target/debug/playground`
[src/main.rs:7] a = 1024

parse() returns a Result<i32, _> so we use ok() to turn in into an Option<i32> loosing a potential error message in the process.

I don't really understand what you are getting at. The original JS code doesn't use callbacks. We only need them in rust because we need to use map and and_then to chain operations on an Option together.

Also the returns of the callbacks explicitly don't need to be Options. That's the case in which you will use map. If they do return Options you use and_then.

Or where you talking about the case in which you want to make any function in your api chainable? Like

let o = f1(x).and_then(f2).and_then(f3);

This topic was automatically closed 90 days after the last reply. We invite you to open a new topic if you have further questions or comments.