For my current project, I found myself wanting to atomically¹ apply several fallible operations. There didn't seem to be a good abstraction for that (at least in std
and a cursory search of crates.io), and so I put one together.
- Is there an existing solution that I've missed?
- Have I built any accidental footguns into the API?
- Is there interest in having this as its own crate, or should I keep it internal to my project?
¹ In the sense that they all succeed or all do nothing, not in the std::sync::atomic
sense.
Edit: I've decided that depending on
Deref
is a mistake; it's better to not require any preview of the committed value-- individual implementations can do what they want. (No updated code yet, though).
Edit 2: The code is now updated to reflect this.
(Playground) Example usage towards the bottom.
/// Represents the result of an operation that can be undone.
///
/// By the time this is created, the operation *must* be complete:
/// Dropping or forgetting this value should leave the original object
/// in a committed state.
trait Revertable {
type Output: Sized;
/// Confirm that no revert will be necessary.
fn commit(x: Self)->Self::Output;
/// Cancel this operation.
///
/// The original object will be returned to an "equivalent" state as
/// before the operation was requested.
fn revert(x: Self);
}
/// A changeset that can be committed or reverted as a unit
trait Changeset {
type Success: Sized;
type Failure: Sized;
/// Attempt to apply this changeset
///
/// If all the changes succeeded, applies them and returns `Ok(_)`.
/// Otherwise, reverts those that succeeded and returns `Err(_)`
fn commit(self)->Result<Self::Success, Self::Failure>;
/// Abandon these changes
///
/// Reverts all successful intermediate results.
fn revert(self)->Self::Failure;
/// Will a call to `commit` succeed?
///
/// Can cause panics if the implementation doesn't agree with `commit`.
fn will_commit(&self)->bool;
}
#[derive(Copy,Clone,Debug,Eq,PartialEq,Hash)]
enum CommitErr<E> {
Reverted,
Err(E)
}
impl<E> From<E> for CommitErr<E> {
fn from(e:E)->Self { CommitErr::Err(e) }
}
impl<T,E> Changeset for Result<T,E>
where T:Revertable {
type Success = T::Output;
type Failure = CommitErr<E>;
fn commit(self)->Result<Self::Success, Self::Failure> {
Ok(Revertable::commit(self?))
}
fn revert(self)->Self::Failure {
match self {
Ok(val) => { Revertable::revert(val); CommitErr::Reverted },
Err(e) => e.into()
}
}
fn will_commit(&self)->bool {
self.is_ok()
}
}
impl<T:Revertable> Changeset for T {
type Success = T::Output;
type Failure = CommitErr<std::convert::Infallible>;
fn commit(self)->Result<Self::Success, Self::Failure> {
Ok(Revertable::commit(self))
}
fn revert(self)->Self::Failure {
Revertable::revert(self);
CommitErr::Reverted
}
fn will_commit(&self)->bool {
true
}
}
macro_rules! two_phase_tuple {
($($metavar:tt)+) => {
impl<$($metavar,)+> Changeset for ($($metavar,)+)
where $( $metavar: Changeset, )+ {
type Success = ($( $metavar::Success, )+);
type Failure = ($( $metavar::Failure, )+);
fn commit(self)->Result<Self::Success, Self::Failure> {
if self.will_commit() {
#[allow(non_snake_case)]
let ( $($metavar,)+ ) = self;
Ok(($(
$metavar.commit().unwrap_or_else(|_|
panic!("{}: Bad will_commit implementation",
std::any::type_name::<$metavar>())
),
)+))
} else {
Err(self.revert())
}
}
fn revert(self)->Self::Failure {
#[allow(non_snake_case)]
let ( $($metavar,)+ ) = self;
($(
$metavar.revert(),
)+)
}
fn will_commit(&self)->bool {
#[allow(non_snake_case)]
let ( ref $($metavar,)+ ) = self;
true $(
&& $metavar.will_commit()
)+
}
}
}
}
two_phase_tuple!{A}
two_phase_tuple!{A B}
two_phase_tuple!{A B C}
two_phase_tuple!{A B C D}
two_phase_tuple!{A B C D E}
two_phase_tuple!{A B C D E F}
two_phase_tuple!{A B C D E F G}
two_phase_tuple!{A B C D E F G H}
two_phase_tuple!{A B C D E F G H I}
two_phase_tuple!{A B C D E F G H I J}
two_phase_tuple!{A B C D E F G H I J K}
two_phase_tuple!{A B C D E F G H I J K L}
two_phase_tuple!{A B C D E F G H I J K L M}
// ======================
// Example Implementation
// ======================
trait VecExt<T> {
fn two_phase_push(&mut self, val:T)->TwoPhasePush<'_,T>;
fn two_phase_pop(&mut self)->Result<TwoPhasePop<'_,T>, EmptyVec>;
}
impl<T> VecExt<T> for Vec<T> {
fn two_phase_push(&mut self, x:T)->TwoPhasePush<'_,T> {
self.push(x);
TwoPhasePush(self)
}
fn two_phase_pop(&mut self)->Result<TwoPhasePop<'_,T>, EmptyVec> {
match self.pop() {
Some(x) => Ok(TwoPhasePop(self, x)),
None => Err(EmptyVec)
}
}
}
struct TwoPhasePush<'a, T>(&'a mut Vec<T>);
impl<'a,T> Revertable for TwoPhasePush<'a,T> {
type Output = ();
fn commit(_: Self)->() { () }
fn revert(x: Self) {
x.0.pop().unwrap();
}
}
#[derive(Copy,Clone,Debug,Eq,PartialEq,Hash)]
struct EmptyVec;
struct TwoPhasePop<'a, T>(&'a mut Vec<T>, T);
impl<'a,T> Revertable for TwoPhasePop<'a,T> {
type Output = T;
fn commit(x: Self)->T { x.1 }
fn revert(x: Self) { x.0.push(x.1) }
}
#[test]
fn test_vec() {
let mut a:Vec<usize> = vec![];
let mut b:Vec<usize> = vec![42];
let mut c:Vec<usize> = vec![];
assert_eq!(
(a.two_phase_push(3), b.two_phase_pop(), c.two_phase_pop()).commit(),
Err((CommitErr::Reverted, CommitErr::Reverted, CommitErr::Err(EmptyVec)))
);
assert_eq!(a, vec![]);
assert_eq!(b, vec![42]);
assert_eq!(c, vec![]);
assert_eq!(
(a.two_phase_push(3), b.two_phase_pop(), c.two_phase_push(7)).commit(),
Ok(( (), 42, () ))
);
assert_eq!(a, vec![3]);
assert_eq!(b, vec![]);
assert_eq!(c, vec![7]);
}