๐Ÿฆ€ Functional Rust
๐ŸŽฌ Rust Ownership in 30 seconds Visual walkthrough of ownership, moves, and automatic memory management.
๐Ÿ“ Text version (for readers / accessibility)

โ€ข Each value in Rust has exactly one owner โ€” when the owner goes out of scope, the value is dropped

โ€ข Assignment moves ownership by default; the original binding becomes invalid

โ€ข Borrowing (&T / &mut T) lets you reference data without taking ownership

โ€ข The compiler enforces: many shared references OR one mutable reference, never both

โ€ข No garbage collector needed โ€” memory is freed deterministically at scope exit

1018: Error Downcast

Difficulty: Advanced Category: Error Handling Concept: Downcasting `Box<dyn Error>` to concrete error types at runtime Key Insight: `downcast_ref::<T>()` recovers the concrete type from a trait object โ€” it's Rust's runtime type check, needed when you use `Box<dyn Error>` but want to handle specific errors differently.
// 1018: Error Downcast
// Downcasting Box<dyn Error> to concrete type

use std::error::Error;
use std::fmt;

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

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

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

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

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

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

// Functions returning type-erased errors
fn might_fail_db() -> Result<(), Box<dyn Error>> {
    Err(Box::new(DatabaseError("timeout".into())))
}

fn might_fail_auth() -> Result<(), Box<dyn Error>> {
    Err(Box::new(AuthError("expired token".into())))
}

// Approach 1: downcast_ref โ€” borrow the concrete type
fn classify_error(err: &(dyn Error + 'static)) -> &'static str {
    if err.downcast_ref::<DatabaseError>().is_some() {
        "database"
    } else if err.downcast_ref::<AuthError>().is_some() {
        "auth"
    } else if err.downcast_ref::<NetworkError>().is_some() {
        "network"
    } else {
        "unknown"
    }
}

// Approach 2: downcast โ€” take ownership of concrete type
fn handle_error(err: Box<dyn Error>) -> String {
    if let Ok(db_err) = err.downcast::<DatabaseError>() {
        format!("Handling DB: {}", db_err.0)
    } else {
        "unhandled error".into()
    }
}

// Approach 3: Type ID check
fn is_database_error(err: &(dyn Error + 'static)) -> bool {
    err.downcast_ref::<DatabaseError>().is_some()
}


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

    #[test]
    fn test_downcast_ref_db() {
        let err: Box<dyn Error> = Box::new(DatabaseError("test".into()));
        assert_eq!(classify_error(err.as_ref()), "database");

        let concrete = err.downcast_ref::<DatabaseError>().unwrap();
        assert_eq!(concrete.0, "test");
    }

    #[test]
    fn test_downcast_ref_auth() {
        let err: Box<dyn Error> = Box::new(AuthError("bad".into()));
        assert_eq!(classify_error(err.as_ref()), "auth");
    }

    #[test]
    fn test_downcast_ref_unknown() {
        let err: Box<dyn Error> = Box::new(std::io::Error::new(
            std::io::ErrorKind::Other, "misc"
        ));
        assert_eq!(classify_error(err.as_ref()), "unknown");
    }

    #[test]
    fn test_downcast_owned() {
        let err: Box<dyn Error> = Box::new(DatabaseError("owned".into()));
        let result = handle_error(err);
        assert_eq!(result, "Handling DB: owned");
    }

    #[test]
    fn test_downcast_owned_wrong_type() {
        let err: Box<dyn Error> = Box::new(AuthError("nope".into()));
        let result = handle_error(err);
        assert_eq!(result, "unhandled error");
    }

    #[test]
    fn test_is_check() {
        let err: Box<dyn Error> = Box::new(DatabaseError("x".into()));
        assert!(is_database_error(err.as_ref()));

        let err: Box<dyn Error> = Box::new(AuthError("x".into()));
        assert!(!is_database_error(err.as_ref()));
    }

    #[test]
    fn test_from_result() {
        let result = might_fail_db();
        let err = result.unwrap_err();
        assert!(err.downcast_ref::<DatabaseError>().is_some());
    }
}
(* 1018: Error Downcast *)
(* OCaml: pattern matching on exception types *)

(* In OCaml, exceptions are already pattern-matchable *)
exception DatabaseError of string
exception AuthError of string
exception NetworkError of string

(* Approach 1: Direct pattern matching on exceptions *)
let handle_error f =
  try Ok (f ())
  with
  | DatabaseError msg -> Error (Printf.sprintf "DB: %s" msg)
  | AuthError msg -> Error (Printf.sprintf "Auth: %s" msg)
  | NetworkError msg -> Error (Printf.sprintf "Net: %s" msg)
  | exn -> Error (Printf.sprintf "Unknown: %s" (Printexc.to_string exn))

(* Approach 2: Using a general exception container *)
type any_error = ..  (* extensible variant type *)
type any_error += DB of string | Auth of string | Net of string

let classify_error (e : any_error) =
  match e with
  | DB msg -> Printf.sprintf "database: %s" msg
  | Auth msg -> Printf.sprintf "auth: %s" msg
  | Net msg -> Printf.sprintf "network: %s" msg
  | _ -> "unknown error"

(* Approach 3: GADT-style typed errors *)
type _ error_kind =
  | DbKind : string error_kind
  | AuthKind : string error_kind

type packed_error = Pack : 'a error_kind * 'a -> packed_error

let describe_packed (Pack (kind, value)) =
  match kind with
  | DbKind -> Printf.sprintf "DB error: %s" value
  | AuthKind -> Printf.sprintf "Auth error: %s" value

let test_exceptions () =
  let r = handle_error (fun () -> raise (DatabaseError "timeout")) in
  assert (r = Error "DB: timeout");
  let r = handle_error (fun () -> 42) in
  assert (r = Ok 42);
  Printf.printf "  Approach 1 (exception matching): passed\n"

let test_extensible () =
  assert (classify_error (DB "conn failed") = "database: conn failed");
  assert (classify_error (Auth "bad token") = "auth: bad token");
  Printf.printf "  Approach 2 (extensible variants): passed\n"

let test_gadt () =
  let e = Pack (DbKind, "query failed") in
  assert (describe_packed e = "DB error: query failed");
  let e = Pack (AuthKind, "expired") in
  assert (describe_packed e = "Auth error: expired");
  Printf.printf "  Approach 3 (GADT packed): passed\n"

let () =
  Printf.printf "Testing error downcast:\n";
  test_exceptions ();
  test_extensible ();
  test_gadt ();
  Printf.printf "โœ“ All tests passed\n"

๐Ÿ“Š Detailed Comparison

Error Downcast โ€” Comparison

Core Insight

Type erasure (`Box<dyn Error>`) is convenient but loses type information. Downcasting recovers it at runtime โ€” OCaml's exception matching does this naturally, while Rust needs explicit downcasts.

OCaml Approach

  • Exceptions are pattern-matchable by default โ€” no "downcast" needed
  • Extensible variant types (`type t += ...`) support open matching
  • GADTs can encode typed error containers
  • Pattern matching is exhaustive (or has wildcard)

Rust Approach

  • `downcast_ref::<T>()` โ€” borrow as concrete type (returns `Option`)
  • `downcast::<T>()` โ€” take ownership (returns `Result`)
  • Uses `TypeId` internally (runtime reflection)
  • Unavoidable when working with `Box<dyn Error>` from libraries

Comparison Table

AspectOCamlRust
Type recoveryPattern matching`downcast_ref` / `downcast`
Compile-time safeYes (match)No (runtime check)
CostZeroTypeId comparison
OwnershipN/A`downcast` consumes Box
Preferred approachExceptions / variantsTyped enum (avoid downcast)