๐Ÿฆ€ Functional Rust

1011: The ? (Try) Operator

Difficulty: Intermediate Category: Error Handling Concept: Deep dive on `?`: early return, automatic `From` conversion, and desugaring Key Insight: `?` is syntactic sugar for `match expr { Ok(v) => v, Err(e) => return Err(From::from(e)) }` โ€” it combines early return with automatic error type conversion.
// 1011: The ? (Try) Operator
// Deep dive: early return, From conversion, desugaring

use std::fmt;
use std::num::ParseIntError;

#[derive(Debug, PartialEq)]
enum AppError {
    NotFound,
    ParseFailed(String),
    TooLarge(i64),
}

impl fmt::Display for AppError {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            AppError::NotFound => write!(f, "not found"),
            AppError::ParseFailed(s) => write!(f, "parse failed: {}", s),
            AppError::TooLarge(n) => write!(f, "too large: {}", n),
        }
    }
}
impl std::error::Error for AppError {}

impl From<ParseIntError> for AppError {
    fn from(e: ParseIntError) -> Self {
        AppError::ParseFailed(e.to_string())
    }
}

fn read_data(key: &str) -> Result<String, AppError> {
    if key == "missing" {
        Err(AppError::NotFound)
    } else {
        Ok("42".into())
    }
}

fn parse_data(s: &str) -> Result<i64, ParseIntError> {
    s.parse::<i64>()
}

fn validate(n: i64) -> Result<i64, AppError> {
    if n > 100 {
        Err(AppError::TooLarge(n))
    } else {
        Ok(n)
    }
}

// Approach 1: The ? operator โ€” what it looks like
fn process_try(key: &str) -> Result<i64, AppError> {
    let s = read_data(key)?;        // early return if Err
    let n = parse_data(&s)?;        // ParseIntError -> AppError via From
    let v = validate(n)?;           // early return if Err
    Ok(v)
}

// Approach 2: What ? desugars to (approximately)
fn process_desugared(key: &str) -> Result<i64, AppError> {
    let s = match read_data(key) {
        Ok(v) => v,
        Err(e) => return Err(From::from(e)),
    };
    let n = match parse_data(&s) {
        Ok(v) => v,
        Err(e) => return Err(From::from(e)), // From<ParseIntError>
    };
    let v = match validate(n) {
        Ok(v) => v,
        Err(e) => return Err(From::from(e)),
    };
    Ok(v)
}

// Approach 3: ? in expression position
fn process_inline(key: &str) -> Result<i64, AppError> {
    validate(parse_data(&read_data(key)?)?)
}

fn main() {
    println!("process_try('ok'): {:?}", process_try("ok"));
    println!("process_try('missing'): {:?}", process_try("missing"));
    println!("Run `cargo test` to verify all examples.");
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_try_success() {
        assert_eq!(process_try("ok"), Ok(42));
    }

    #[test]
    fn test_try_not_found() {
        assert_eq!(process_try("missing"), Err(AppError::NotFound));
    }

    #[test]
    fn test_desugared_matches_try() {
        assert_eq!(process_try("ok"), process_desugared("ok"));
        assert_eq!(process_try("missing"), process_desugared("missing"));
    }

    #[test]
    fn test_inline_matches_try() {
        assert_eq!(process_try("ok"), process_inline("ok"));
        assert_eq!(process_try("missing"), process_inline("missing"));
    }

    #[test]
    fn test_from_conversion() {
        // ? calls From::from on the error
        let parse_err = "abc".parse::<i64>().unwrap_err();
        let app_err: AppError = parse_err.into();
        assert!(matches!(app_err, AppError::ParseFailed(_)));
    }

    #[test]
    fn test_try_in_closure() {
        // ? works in closures that return Result
        let process = |key: &str| -> Result<i64, AppError> {
            let s = read_data(key)?;
            Ok(s.parse::<i64>()?)
        };
        assert_eq!(process("ok"), Ok(42));
    }

    #[test]
    fn test_too_large() {
        // If we had data "200", validate would fail
        fn process_large() -> Result<i64, AppError> {
            let n: i64 = "200".parse().map_err(|e: ParseIntError| AppError::ParseFailed(e.to_string()))?;
            validate(n)
        }
        assert_eq!(process_large(), Err(AppError::TooLarge(200)));
    }
}
(* 1011: The Try Operator (?) *)
(* OCaml has no ? โ€” we simulate with bind/let* *)

type error = NotFound | ParseFailed of string | TooLarge of int

(* Approach 1: Nested match โ€” what Rust's ? replaces *)
let read_data key =
  if key = "missing" then Error NotFound
  else Ok "42"

let parse_data s =
  match int_of_string_opt s with
  | None -> Error (ParseFailed s)
  | Some n -> Ok n

let validate n =
  if n > 100 then Error (TooLarge n)
  else Ok n

let process_nested key =
  match read_data key with
  | Error e -> Error e
  | Ok s ->
    match parse_data s with
    | Error e -> Error e
    | Ok n ->
      match validate n with
      | Error e -> Error e
      | Ok v -> Ok v

(* Approach 2: bind operator โ€” monadic chaining *)
let ( >>= ) r f = match r with Ok v -> f v | Error e -> Error e

let process_bind key =
  read_data key >>= parse_data >>= validate

(* Approach 3: let* syntax (OCaml 4.08+ binding operators) *)
let ( let* ) = Result.bind

let process_let_star key =
  let* s = read_data key in
  let* n = parse_data s in
  let* v = validate n in
  Ok v

let test_nested () =
  assert (process_nested "ok" = Ok 42);
  assert (process_nested "missing" = Error NotFound);
  Printf.printf "  Approach 1 (nested match): passed\n"

let test_bind () =
  assert (process_bind "ok" = Ok 42);
  assert (process_bind "missing" = Error NotFound);
  Printf.printf "  Approach 2 (bind operator): passed\n"

let test_let_star () =
  assert (process_let_star "ok" = Ok 42);
  assert (process_let_star "missing" = Error NotFound);
  Printf.printf "  Approach 3 (let* syntax): passed\n"

let () =
  Printf.printf "Testing try operator equivalents:\n";
  test_nested ();
  test_bind ();
  test_let_star ();
  Printf.printf "โœ“ All tests passed\n"

๐Ÿ“Š Detailed Comparison

The ? (Try) Operator โ€” Comparison

Core Insight

Rust's `?` is the ergonomic equivalent of OCaml's `let*` binding operator โ€” both eliminate nested match/bind chains while keeping error handling explicit and typed.

OCaml Approach

  • Nested `match` expressions (verbose but clear)
  • `>>=` bind operator (monadic, Haskell-style)
  • `let*` binding operators (OCaml 4.08+, most ergonomic)
  • All three require the same error type throughout

Rust Approach

  • `?` operator: `expr?` desugars to match + early return + `From::from(e)`
  • Automatic `From` conversion means different error types can coexist
  • Works in any function returning `Result` (or `Option`)
  • Can be used in expression position: `parse(&read(key)?)?`

Comparison Table

AspectOCaml `let`Rust `?`
Syntax`let x = expr in``let x = expr?;`
Error conversionNone (same type required)Automatic via `From`
Early returnVia continuationVia `return Err(...)`
NestingFlat (monadic)Flat (early return)
Works onResult (custom)Result, Option
Available sinceOCaml 4.08Rust 1.13 (`try!` macro before)