1008-option-to-result — Option to Result
Tutorial
The Problem
Option<T> represents the presence or absence of a value, while Result<T, E> represents success or a specific failure reason. Real programs often start with an Option lookup — for example, reading a key from a map — and need to convert it into a Result that can carry an error message and propagate through the ? operator.
Rust provides two conversion methods: ok_or (eager, always constructs the error value) and ok_or_else (lazy, only constructs the error if None is encountered). This distinction matters for performance when the error value is expensive to build.
🎯 Learning Outcomes
Option versus Result for absent valuesOption<T> to Result<T, E> using ok_or and ok_or_elseOption-to-Result conversions inside ?-based pipelinesResult::ok() to collapse Result back to OptionCode Example
#![allow(clippy::all)]
// 1008: Option to Result Conversion
// Convert Option<T> to Result<T, E> with ok_or / ok_or_else
use std::collections::HashMap;
fn build_users() -> HashMap<String, (String, u32)> {
let mut m = HashMap::new();
m.insert("Alice".into(), ("alice@ex.com".into(), 30));
m.insert("Bob".into(), ("bob@ex.com".into(), 17));
m
}
// Approach 1: ok_or — eager error value
fn find_user_eager<'a>(
users: &'a HashMap<String, (String, u32)>,
name: &str,
) -> Result<&'a (String, u32), String> {
users.get(name).ok_or(format!("user not found: {}", name))
}
// Approach 2: ok_or_else — lazy error (avoids allocation if Some)
fn find_user_lazy<'a>(
users: &'a HashMap<String, (String, u32)>,
name: &str,
) -> Result<&'a (String, u32), String> {
users
.get(name)
.ok_or_else(|| format!("user not found: {}", name))
}
// Approach 3: Chaining Option->Result in a pipeline
fn find_and_validate(
users: &HashMap<String, (String, u32)>,
name: &str,
min_age: u32,
) -> Result<(String, u32), String> {
users
.get(name)
.ok_or_else(|| format!("user not found: {}", name))
.and_then(|(email, age)| {
if *age >= min_age {
Ok((email.clone(), *age))
} else {
Err(format!("{} is too young ({} < {})", name, age, min_age))
}
})
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_ok_or_found() {
let users = build_users();
let result = find_user_eager(&users, "Alice");
assert!(result.is_ok());
assert_eq!(result.unwrap().1, 30);
}
#[test]
fn test_ok_or_not_found() {
let users = build_users();
let result = find_user_eager(&users, "Unknown");
assert_eq!(result.unwrap_err(), "user not found: Unknown");
}
#[test]
fn test_ok_or_else_lazy() {
let users = build_users();
assert!(find_user_lazy(&users, "Bob").is_ok());
assert!(find_user_lazy(&users, "Nobody").is_err());
}
#[test]
fn test_validate_success() {
let users = build_users();
let result = find_and_validate(&users, "Alice", 18);
assert_eq!(result.unwrap(), ("alice@ex.com".into(), 30));
}
#[test]
fn test_validate_too_young() {
let users = build_users();
let result = find_and_validate(&users, "Bob", 18);
assert!(result.unwrap_err().contains("too young"));
}
#[test]
fn test_validate_not_found() {
let users = build_users();
let result = find_and_validate(&users, "Nobody", 18);
assert!(result.unwrap_err().contains("not found"));
}
#[test]
fn test_option_methods() {
// Direct Option -> Result conversions
assert_eq!(Some(42).ok_or("missing"), Ok(42));
assert_eq!(None::<i32>.ok_or("missing"), Err("missing"));
// Result -> Option conversions
assert_eq!(Ok::<i32, &str>(42).ok(), Some(42));
assert_eq!(Err::<i32, &str>("fail").ok(), None);
}
}Key Differences
ok_or / ok_or_else as inherent methods; OCaml requires manual matching or a library helper.? integration**: Rust's ? can be applied directly to Result after conversion; OCaml's equivalent (let*) requires the result to already be in the monad.OCaml Approach
OCaml has no ok_or method on Option, but the pattern is one line:
let option_to_result opt msg =
match opt with
| Some v -> Ok v
| None -> Error msg
Libraries like Base provide Option.value_exn and Option.to_or_error. The lazy variant is expressed with a thunk: None -> Error (msg ()).
Full Source
#![allow(clippy::all)]
// 1008: Option to Result Conversion
// Convert Option<T> to Result<T, E> with ok_or / ok_or_else
use std::collections::HashMap;
fn build_users() -> HashMap<String, (String, u32)> {
let mut m = HashMap::new();
m.insert("Alice".into(), ("alice@ex.com".into(), 30));
m.insert("Bob".into(), ("bob@ex.com".into(), 17));
m
}
// Approach 1: ok_or — eager error value
fn find_user_eager<'a>(
users: &'a HashMap<String, (String, u32)>,
name: &str,
) -> Result<&'a (String, u32), String> {
users.get(name).ok_or(format!("user not found: {}", name))
}
// Approach 2: ok_or_else — lazy error (avoids allocation if Some)
fn find_user_lazy<'a>(
users: &'a HashMap<String, (String, u32)>,
name: &str,
) -> Result<&'a (String, u32), String> {
users
.get(name)
.ok_or_else(|| format!("user not found: {}", name))
}
// Approach 3: Chaining Option->Result in a pipeline
fn find_and_validate(
users: &HashMap<String, (String, u32)>,
name: &str,
min_age: u32,
) -> Result<(String, u32), String> {
users
.get(name)
.ok_or_else(|| format!("user not found: {}", name))
.and_then(|(email, age)| {
if *age >= min_age {
Ok((email.clone(), *age))
} else {
Err(format!("{} is too young ({} < {})", name, age, min_age))
}
})
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_ok_or_found() {
let users = build_users();
let result = find_user_eager(&users, "Alice");
assert!(result.is_ok());
assert_eq!(result.unwrap().1, 30);
}
#[test]
fn test_ok_or_not_found() {
let users = build_users();
let result = find_user_eager(&users, "Unknown");
assert_eq!(result.unwrap_err(), "user not found: Unknown");
}
#[test]
fn test_ok_or_else_lazy() {
let users = build_users();
assert!(find_user_lazy(&users, "Bob").is_ok());
assert!(find_user_lazy(&users, "Nobody").is_err());
}
#[test]
fn test_validate_success() {
let users = build_users();
let result = find_and_validate(&users, "Alice", 18);
assert_eq!(result.unwrap(), ("alice@ex.com".into(), 30));
}
#[test]
fn test_validate_too_young() {
let users = build_users();
let result = find_and_validate(&users, "Bob", 18);
assert!(result.unwrap_err().contains("too young"));
}
#[test]
fn test_validate_not_found() {
let users = build_users();
let result = find_and_validate(&users, "Nobody", 18);
assert!(result.unwrap_err().contains("not found"));
}
#[test]
fn test_option_methods() {
// Direct Option -> Result conversions
assert_eq!(Some(42).ok_or("missing"), Ok(42));
assert_eq!(None::<i32>.ok_or("missing"), Err("missing"));
// Result -> Option conversions
assert_eq!(Ok::<i32, &str>(42).ok(), Some(42));
assert_eq!(Err::<i32, &str>("fail").ok(), None);
}
}#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_ok_or_found() {
let users = build_users();
let result = find_user_eager(&users, "Alice");
assert!(result.is_ok());
assert_eq!(result.unwrap().1, 30);
}
#[test]
fn test_ok_or_not_found() {
let users = build_users();
let result = find_user_eager(&users, "Unknown");
assert_eq!(result.unwrap_err(), "user not found: Unknown");
}
#[test]
fn test_ok_or_else_lazy() {
let users = build_users();
assert!(find_user_lazy(&users, "Bob").is_ok());
assert!(find_user_lazy(&users, "Nobody").is_err());
}
#[test]
fn test_validate_success() {
let users = build_users();
let result = find_and_validate(&users, "Alice", 18);
assert_eq!(result.unwrap(), ("alice@ex.com".into(), 30));
}
#[test]
fn test_validate_too_young() {
let users = build_users();
let result = find_and_validate(&users, "Bob", 18);
assert!(result.unwrap_err().contains("too young"));
}
#[test]
fn test_validate_not_found() {
let users = build_users();
let result = find_and_validate(&users, "Nobody", 18);
assert!(result.unwrap_err().contains("not found"));
}
#[test]
fn test_option_methods() {
// Direct Option -> Result conversions
assert_eq!(Some(42).ok_or("missing"), Ok(42));
assert_eq!(None::<i32>.ok_or("missing"), Err("missing"));
// Result -> Option conversions
assert_eq!(Ok::<i32, &str>(42).ok(), Some(42));
assert_eq!(Err::<i32, &str>("fail").ok(), None);
}
}
Deep Comparison
Option to Result — Comparison
Core Insight
Both languages need to bridge the gap between "absence" (Option/None) and "error" (Result/Error). The conversion adds semantic meaning to what was just a missing value.
OCaml Approach
option and return Ok/Errorok_or and ok_or_else helpers (not in stdlib before 4.08)fun () -> error_value thunkRust Approach
Option::ok_or(err) and Option::ok_or_else(|| err)Result::ok() and Result::err() to get Options.ok_or_else(|| ...)?.and_then(...)Comparison Table
| Aspect | OCaml | Rust |
|---|---|---|
| Option to Result | Custom helper or match | .ok_or() / .ok_or_else() |
| Result to Option | Custom helper | .ok() / .err() |
| Lazy error | fun () -> ... thunk | \|\| ... closure |
| Chaining | \|> pipeline | .method() chain |
| In stdlib | Since 4.08 (partial) | Always available |
Exercises
find_user_by_email function that searches the map by email value instead of key, returning Option<&str> converted to Result.find_and_validate for multiple names, collecting all successful results into a Vec.Result<User, String> and maps it to Option<User>, discarding the error reason.