๐Ÿฆ€ Functional Rust
๐ŸŽฌ Error Handling in Rust Option, Result, the ? operator, and combinators.
๐Ÿ“ Text version (for readers / accessibility)

โ€ข Option represents a value that may or may not exist โ€” Some(value) or None

โ€ข Result represents success (Ok) or failure (Err) โ€” no exceptions needed

โ€ข The ? operator propagates errors up the call stack concisely

โ€ข Combinators like .map(), .and_then(), .unwrap_or() chain fallible operations

โ€ข The compiler forces you to handle every error case โ€” no silent failures

1006: Multiple Error Types

Difficulty: Intermediate Category: Error Handling Concept: Unifying multiple error types using `Box<dyn Error>` vs typed enum approach Key Insight: `Box<dyn Error>` is fast to write but loses pattern matching; typed enums are more work upfront but give exhaustive matching and zero-cost abstraction.
// 1006: Multiple Error Types
// Unifying multiple error types: Box<dyn Error> vs enum approach

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

// Individual error types
#[derive(Debug)]
struct IoError(String);

impl fmt::Display for IoError {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(f, "IO error: {}", self.0)
    }
}
impl std::error::Error for IoError {}

#[derive(Debug)]
struct NetError(String);

impl fmt::Display for NetError {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(f, "network error: {}", self.0)
    }
}
impl std::error::Error for NetError {}

// Approach 1: Box<dyn Error> โ€” quick and flexible
fn do_io_boxed() -> Result<String, Box<dyn std::error::Error>> {
    Err(Box::new(IoError("file not found".into())))
}

fn do_parse_boxed(s: &str) -> Result<i64, Box<dyn std::error::Error>> {
    let n: i64 = s.parse()?; // ParseIntError auto-boxed
    Ok(n)
}

fn do_net_boxed() -> Result<String, Box<dyn std::error::Error>> {
    Err(Box::new(NetError("timeout".into())))
}

fn process_boxed() -> Result<i64, Box<dyn std::error::Error>> {
    let data = do_io_boxed().or_else(|_| Ok::<_, Box<dyn std::error::Error>>("42".into()))?;
    let parsed = do_parse_boxed(&data)?;
    let _response = do_net_boxed().or_else(|_| Ok::<String, Box<dyn std::error::Error>>("ok".into()))?;
    Ok(parsed)
}

// Approach 2: Typed enum โ€” exhaustive matching
#[derive(Debug)]
enum AppError {
    Io(IoError),
    Parse(ParseIntError),
    Net(NetError),
}

impl fmt::Display for AppError {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            AppError::Io(e) => write!(f, "{}", e),
            AppError::Parse(e) => write!(f, "parse: {}", e),
            AppError::Net(e) => write!(f, "{}", e),
        }
    }
}
impl std::error::Error for AppError {}

impl From<IoError> for AppError {
    fn from(e: IoError) -> Self { AppError::Io(e) }
}
impl From<ParseIntError> for AppError {
    fn from(e: ParseIntError) -> Self { AppError::Parse(e) }
}
impl From<NetError> for AppError {
    fn from(e: NetError) -> Self { AppError::Net(e) }
}

fn do_io_typed() -> Result<String, IoError> {
    Ok("42".into())
}

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

fn process_typed() -> Result<i64, AppError> {
    let data = do_io_typed()?;     // IoError -> AppError
    let parsed = do_parse_typed(&data)?; // ParseIntError -> AppError
    Ok(parsed)
}


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

    #[test]
    fn test_boxed_error() {
        let result = process_boxed();
        assert_eq!(result.unwrap(), 42);
    }

    #[test]
    fn test_boxed_io_error() {
        let err = do_io_boxed().unwrap_err();
        assert!(err.to_string().contains("IO error"));
    }

    #[test]
    fn test_typed_success() {
        assert_eq!(process_typed().unwrap(), 42);
    }

    #[test]
    fn test_typed_pattern_match() {
        let err: AppError = IoError("test".into()).into();
        assert!(matches!(err, AppError::Io(_)));

        let err: AppError = "abc".parse::<i64>().unwrap_err().into();
        assert!(matches!(err, AppError::Parse(_)));
    }

    #[test]
    fn test_boxed_parse_error() {
        let result = do_parse_boxed("not_a_number");
        assert!(result.is_err());
    }

    #[test]
    fn test_display_format() {
        let err = AppError::Net(NetError("timeout".into()));
        assert_eq!(err.to_string(), "network error: timeout");
    }
}
(* 1006: Multiple Error Types *)
(* Unifying multiple error types into one *)

type io_error = FileNotFound | ReadError of string
type parse_error = BadFormat | Overflow
type net_error = Timeout | ConnectionRefused

(* Approach 1: Unified variant type (like Rust enum) *)
type app_error =
  | Io of io_error
  | Parse of parse_error
  | Net of net_error

let do_io () = Error (Io FileNotFound)
let do_parse () = Error (Parse BadFormat)
let do_net () = Error (Net Timeout)

let process_all () =
  match do_io () with
  | Error _ as e -> e
  | Ok data ->
    match do_parse () with
    | Error _ as e -> e
    | Ok parsed ->
      match do_net () with
      | Error _ as e -> e
      | Ok _ -> Ok (data, parsed)

(* Approach 2: Polymorphic variants โ€” open, extensible *)
let do_io_poly () : (string, [> `FileNotFound | `ReadError of string]) result =
  Error `FileNotFound

let do_parse_poly () : (int, [> `BadFormat | `Overflow]) result =
  Ok 42

let process_poly () =
  match do_io_poly () with
  | Error _ as e -> e
  | Ok _data ->
    match do_parse_poly () with
    | Error _ as e -> e
    | Ok v -> Ok v

let test_unified () =
  (match do_io () with
   | Error (Io FileNotFound) -> ()
   | _ -> assert false);
  (match do_parse () with
   | Error (Parse BadFormat) -> ()
   | _ -> assert false);
  (match do_net () with
   | Error (Net Timeout) -> ()
   | _ -> assert false);
  Printf.printf "  Approach 1 (unified variant): passed\n"

let test_poly () =
  (match do_io_poly () with
   | Error `FileNotFound -> ()
   | _ -> assert false);
  assert (do_parse_poly () = Ok 42);
  Printf.printf "  Approach 2 (polymorphic variants): passed\n"

let () =
  Printf.printf "Testing multiple error types:\n";
  test_unified ();
  test_poly ();
  Printf.printf "โœ“ All tests passed\n"

๐Ÿ“Š Detailed Comparison

Multiple Error Types โ€” Comparison

Core Insight

When functions call code with different error types, you need a unification strategy. Rust offers two: type-erased (`Box<dyn Error>`) and typed (enum with `From` impls).

OCaml Approach

  • Unified variant types manually wrap each sub-error
  • Polymorphic variants auto-unify but lose exhaustiveness guarantees
  • No standard trait object equivalent to `Box<dyn Error>`

Rust Approach

  • `Box<dyn Error>`: any error type auto-converts, but you lose pattern matching
  • Typed enum + `From` impls: more boilerplate, full pattern matching retained
  • The `?` operator works with both approaches

Comparison Table

AspectOCaml VariantOCaml Poly VariantRust `Box<dyn>`Rust Enum
Setup costMediumLowLowMedium
Pattern matchingYesPartialNo (need downcast)Yes, exhaustive
ExtensibilityClosedOpenOpenClosed
PerformanceZero-costZero-costHeap allocationZero-cost
Best forLibrariesPrototypingScripts/prototypesLibraries/apps