Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

What is the difference between "context" and "with_context" in anyhow?

This is the documentation for anyhow's Context:

/// Wrap the error value with additional context.
fn context<C>(self, context: C) -> Result<T, Error>
where
    C: Display + Send + Sync + 'static; 
/// Wrap the error value with additional context that is evaluated lazily
/// only once an error does occur.
fn with_context<C, F>(self, f: F) -> Result<T, Error>
where
    C: Display + Send + Sync + 'static,
    F: FnOnce() -> C;

In practice, the difference is that with_context requires a closure, as shown in anyhow's README:

use anyhow::{Context, Result};

fn main() -> Result<()> {
    // ...
    it.detach().context("Failed to detach the important thing")?;

    let content = std::fs::read(path)
        .with_context(|| format!("Failed to read instrs from {}", path))?;
    // ...
}

But it looks like I can replace the with_context method with context, get rid of the closure by deleting ||, and the behaviour of the program wouldn't change.

What is the difference between the two methods under the hood?

like image 663
Paul Razvan Berg Avatar asked Dec 26 '20 19:12

Paul Razvan Berg


2 Answers

The closure provided to with_context is evaluated lazily, and the reason you'd use with_context over context is the same reason you'd choose to lazily evaluate anything: it rarely happens and it's expensive to compute. Once those conditions are satisfied then with_context becomes preferable over context. Commented pseudo-example:

fn calculate_expensive_context() -> Result<()> {
    // really expensive
    std::thread::sleep(std::time::Duration::from_secs(1));
    todo!()
}

// eagerly evaluated expensive context
// this function ALWAYS takes 1+ seconds to execute
// consistently terrible performance
fn failable_operation_eager_context(some_struct: Struct) -> Result<()> {
    some_struct
        .some_failable_action()
        .context(calculate_expensive_context())
}

// lazily evaluated expensive context
// function returns instantly, only takes 1+ seconds on failure
// great performance for average case, only terrible performance on error cases
fn failable_operation_lazy_context(some_struct: Struct) -> Result<()> {
    some_struct
        .some_failable_action()
        .with_context(|| calculate_expensive_context())
}
like image 102
pretzelhammer Avatar answered Oct 19 '22 15:10

pretzelhammer


As the documentation for anyhow::Context::with_context states:

Wrap the error value with additional context that is evaluated lazily only once an error does occur.

If what is passed to context might be computationally expensive, it is better to use with_context, as the closure passed is evaluated only when with_context is called. This is referred to as being evaluated in a lazy and not eager manner.

Similar behavior exists in the standard library, e.g:

Eager Lazy
Option::or Option::or_else
Option::unwrap_or Option::unwrap_or_else
Option::map_or Option::map_or_else
like image 23
Jason Avatar answered Oct 19 '22 15:10

Jason