-
Notifications
You must be signed in to change notification settings - Fork 10
Recover from errors with move semantics #43
Description
I tried out exn in a project of mine to see how this error handling paradigm feels. I like how there is much less boiler plate created by meticulously tracking all error cases in an enum using thiserror. On the other hand a lot of type information is lost and now it becomes a runtime issue whether you get back the correct values from your error context. In my opinion, this hurts quite a lot, and it seems other people have similar experiences (like here).
In particular, I am interested in a way to recover from an error that does not reqire to clone out your value from the error tree. I like to design my programs around Affine Types, i.e. non-clonable objects which is in a way the default property of rust's structs. This property is quite handy in many places as many things are either handled once or get transformed into the next object. This makes the compiler enforce your domain model as you design it. At other times, an object may not be clonable at all, such as complex objects holding threads or mutexes, or objects holding a lot of data. When performing a task with these objects, they often get consumed by a method. But now we also need to handle errors of these methods, from which we may want to recover without just dropping the value.
To give you an idea how this style works, here is a simple example from my project on how I use affine types to act as a witness for some property.
#[derive(Debug, Clone)]
struct Directory(PathBuf);
impl Directory {
fn new(path: PathBuf) -> Result<Self, PathBuf> {
match path.is_dir() {
true => Ok(Self(path)),
false => Err(path),
}
}
}This example may be very simple and here it would also not hurt to clone. However, at other times the struct may represent a task that should only be successfully completed once.
pub enum TaskDescription {
DoA,
DoB,
DoC,
}
pub struct Task {
desc: TaskDescription,
}
pub enum TaskError {
Recoverable(Task),
Impossible,
}
impl Task {
pub fn new(desc: TaskDescription) -> Self {
Self { desc }
}
pub fn run(self) -> Result<(), TaskError> {
use TaskDescription::*;
match self.desc {
DoA => Err(TaskError::Recoverable(self)),
DoB => Err(TaskError::Impossible),
DoC => Ok(()),
}
}
}As far as I can see, we are only able to get a shared reference inside the error tree. So to be able to move items out of the error tree right now, one needs to create an error with a RefCell<Option<T>> field, which is not really nice especially as this is code written by users. However, a mutable reference would not change much either, as we still have to keep the Option<T>, which will still only check on runtime for correct usage such as not moving out a value twice.
As I came to like the approach of exn for non-recoverable situations, I would like to have support for these situations as well. I did some experiments and will link a pull request here as reference (see #44). A way to recover from errors with move semantics, combined with an untyped error (#35) for error messages used all the way throughout the application or even library, seems to be a promising paradigm to use in my programs from now on.