Skip to content

Instantly share code, notes, and snippets.

@yaahc
Last active March 17, 2021 23:35
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save yaahc/c9348cca4aa34ba8bc92f41ba543919d to your computer and use it in GitHub Desktop.
Save yaahc/c9348cca4aa34ba8bc92f41ba543919d to your computer and use it in GitHub Desktop.
proof of concept showing convenient `?` propagation without having a generic `From` impl on the inner error type
// This won't compile by on nightly yet. If you're interested in playing around
// with the code check out `rustup-toolchain-install-master` for installing
// custom toolchains based on automated rust builds, the SHA for the build I
// used in testing is `58f7d3f8ad2b6f1192b6623a4bcd45416dae0d9d`.
//
// ```console
// cargo install rustup-toolchain-install-master
// rustup-toolchain-install-master -- 58f7d3f8ad2b6f1192b6623a4bcd45416dae0d9d
// rustup override set 58f7d3f8ad2b6f1192b6623a4bcd45416dae0d9d
// ```
#![feature(try_trait_v2)]
#![feature(termination_trait_lib)]
#![feature(never_type)]
#![feature(control_flow_enum)]
#![feature(box_patterns)]
#![feature(exhaustive_patterns)]
#![feature(backtrace)]
#![feature(error_iter)]
use std::backtrace::Backtrace;
use std::error::Error;
use std::fmt;
type BoxError = Box<dyn Error + Send + Sync + 'static>;
// =======================================================
#[derive(Debug)]
pub struct DynError {
error: BoxError,
backtrace: Option<Backtrace>,
}
impl fmt::Display for DynError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
self.error.fmt(f)
}
}
/// This type _does_ implement error
impl Error for DynError {
fn source(&self) -> Option<&(dyn Error + 'static)> {
self.error.source()
}
fn backtrace(&self) -> Option<&Backtrace> {
self.backtrace
.as_ref()
.or_else(|| Error::chain(&*self.error).find_map(|error| error.backtrace()))
}
}
impl DynError {
fn new<E>(error: E) -> Self
where
BoxError: From<E>,
{
let error = BoxError::from(error);
// capture a backtrace if the inner error has not
let backtrace = Error::chain(&*error)
.all(|error| error.backtrace().is_none())
.then(Backtrace::capture);
// This logic is necessary for round tripping through `Result<T,
// BoxError>`, as demonstrated in `fn thing_3()`
//
// This is effectively resolving the "overlap rule" issue with `Box<dyn
// Error + ...>` at runtime by always boxing it and then checking if it
// shouldn't have after the fact with `downcast`.
//
// Check if the erased error type is already the type we want
match error.downcast::<DynError>() {
// If it is use it directly
Ok(box error) => error,
// otherwise create a new `DynError` to wrap the type erased error
Err(error) => DynError { backtrace, error },
}
}
}
// =======================================================
mod report {
use super::{BoxError, DynError};
use std::ops::{ControlFlow, FromResidual, Try};
use std::process::Termination;
/// Result that always converts error types to an `DynError`
pub enum DynResult<T> {
Ok(T),
Err(DynError),
}
impl<T> Termination for DynResult<T> {
fn report(self) -> i32 {
match self {
DynResult::Ok(_) => 0,
DynResult::Err(error) => {
eprintln!("Error: {}", error);
eprintln!("Error: {:?}", error);
1
}
}
}
}
impl<T> Try for DynResult<T> {
type Ok = T;
type Residual = DynResult<!>;
fn from_output(value: T) -> Self {
DynResult::Ok(value)
}
fn branch(self) -> ControlFlow<DynResult<!>, T> {
match self {
DynResult::Ok(value) => ControlFlow::Continue(value),
DynResult::Err(error) => ControlFlow::Break(DynResult::Err(error)),
}
}
}
impl<T, E> FromResidual<Result<!, E>> for DynResult<T>
where
BoxError: From<E>,
{
fn from_residual(inner: Result<!, E>) -> Self {
let Err(error) = inner;
let error = DynError::new(error);
DynResult::Err(error)
}
}
impl<T> FromResidual<DynResult<!>> for DynResult<T> {
fn from_residual(residual: DynResult<!>) -> Self {
let DynResult::Err(error) = residual;
DynResult::Err(error)
}
}
impl<T> FromResidual<DynResult<!>> for Result<T, BoxError> {
fn from_residual(residual: DynResult<!>) -> Self {
let DynResult::Err(error) = residual;
let error = BoxError::from(error);
Err(error)
}
}
}
// =======================================================
use report::DynResult;
/// innermost function that uses standard Result
fn do_thing() -> Result<(), InnerError> {
Err(InnerError)
}
/// function that converts `InnerError` into a `DynError` via the `Result`
/// wrapper`
fn do_thing2() -> DynResult<()> {
do_thing()?;
DynResult::Ok(())
}
/// function that round trips the `DynError` through a box error
fn do_thing3() -> Result<(), BoxError> {
do_thing2()?;
Ok(())
}
/// main function that returns and reports the error, also implicitly extracts
/// the DynError back from the `BoxError` it was wrapped in
///
/// # Output
///
/// ```
/// ❯ cargo run
/// Compiling try-tests v0.1.0
/// Finished dev [unoptimized + debuginfo] target(s) in 0.21s
/// Running `target/debug/try-tests`
/// Error: fake inner error that gets type erased and wrapped many times
/// Error: DynError { error: InnerError, backtrace: Some(<disabled>) }
/// ```
fn main() -> DynResult<()> {
do_thing3()?;
DynResult::Ok(())
}
/// random error type
#[derive(Debug, thiserror::Error)]
#[error("fake inner error that gets type erased and wrapped many times")]
struct InnerError;
@bstrie
Copy link

bstrie commented Mar 17, 2021

- type Ok = T;
+ type Output = T;

@yaahc
Copy link
Author

yaahc commented Mar 17, 2021

I'm guessing that would make this not compile with the specific crater build version I mentioned in the top level comment so I'm hesitant to apply this change @bstrie

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment