diff --git a/src/signal/mutable.rs b/src/signal/mutable.rs
index 37b3c08..fca3953 100644
--- a/src/signal/mutable.rs
+++ b/src/signal/mutable.rs
@@ -1,6 +1,7 @@
use super::Signal;
use std;
use std::fmt;
+use std::panic;
use std::pin::Pin;
use std::marker::Unpin;
use std::ops::{Deref, DerefMut};
@@ -310,6 +311,40 @@ impl Mutable {
}
}
+ /// Conditionally modify the value in place
+ ///
+ /// Borrows the value mutably and modifies it in place.
+ ///
+ /// The value is only considered as modified if the modifying closure
+ /// returned `true`. Otherwise the value is assumed to be unmodified
+ /// despite the mutable borrow before.
+ ///
+ /// Returns the result of the closure, i.e. if the value has been modified.
+ pub fn modify(&self, modify: F) -> bool
+ where
+ F: FnOnce(&mut A) -> bool,
+ {
+ let mut locked_state = self.state().lock.write().unwrap();
+
+ let result =
+ panic::catch_unwind(panic::AssertUnwindSafe(|| modify(&mut locked_state.value)));
+ // If modify() panicked forward the panic to the caller without poisoning the lock.
+ match result {
+ Ok(modified) => {
+ if modified {
+ locked_state.notify(modified);
+ }
+ modified
+ }
+ Err(panicked) => {
+ // Drop the lock to avoid poisoning it.
+ drop(locked_state);
+ // Forward the panic to the caller.
+ panic::resume_unwind(panicked);
+ }
+ }
+ }
+
// TODO lots of unit tests to verify that it only notifies when the object is mutated
// TODO return Result ?
// TODO should this inline ?
diff --git a/tests/signal.rs b/tests/signal.rs
index 0677882..086b179 100644
--- a/tests/signal.rs
+++ b/tests/signal.rs
@@ -530,3 +530,54 @@ fn test_throttle_timing() {
assert_eq!(output.poll_change(cx), Poll::Ready(None));
});
}
+
+#[test]
+fn test_modify() {
+ #[derive(Debug, Clone, Copy, PartialEq)]
+ struct State {
+ counter: usize,
+ }
+
+ let inc_counter_if_odd = |state: &mut State| {
+ if state.counter % 2 == 1 {
+ state.counter += 1;
+ return true;
+ }
+ false
+ };
+
+ util::with_noop_context(|cx| {
+ let state = Mutable::new(State { counter: 1 });
+ let initial_state = state.get();
+ let modified_state = {
+ let mut modified_state = initial_state;
+ assert!(inc_counter_if_odd(&mut modified_state));
+ modified_state
+ };
+ assert_ne!(initial_state, modified_state);
+
+ let signal = state.signal();
+ pin_mut!(signal);
+
+ // Initial value
+ assert_eq!(
+ signal.as_mut().poll_change(cx),
+ Poll::Ready(Some(initial_state))
+ );
+
+ // Modified
+ assert_eq!(signal.as_mut().poll_change(cx), Poll::Pending);
+ assert!(state.modify(inc_counter_if_odd));
+ assert_eq!(state.get(), modified_state);
+ assert_eq!(
+ signal.as_mut().poll_change(cx),
+ Poll::Ready(Some(modified_state))
+ );
+
+ // Unmodified
+ assert_eq!(signal.as_mut().poll_change(cx), Poll::Pending);
+ assert!(!state.modify(inc_counter_if_odd));
+ assert_eq!(state.get(), modified_state);
+ assert_eq!(signal.as_mut().poll_change(cx), Poll::Pending);
+ });
+}