replace Option CRDT by explicit CancelingOption and MergingOption types

This commit is contained in:
Alex Auvolat
2026-05-04 19:51:35 +02:00
parent eb37a3e11a
commit bf0a24ea69
20 changed files with 237 additions and 133 deletions
-22
View File
@@ -26,28 +26,6 @@ pub trait Crdt {
fn merge(&mut self, other: &Self);
}
/// `Option<T>` implements Crdt for any type T, even if T doesn't implement CRDT itself: when
/// different values are detected, they are always merged to None. This can be used for value
/// types which shoulnd't be merged, instead of trying to merge things when we know we don't want
/// to merge them (which is what the `AutoCrdt` trait is used for most of the time). This cases
/// arises very often, for example with a Lww or a `LwwMap`: the value type has to be a CRDT so that
/// we have a rule for what to do when timestamps aren't enough to disambiguate (in a distributed
/// system, anything can happen!), and with `AutoCrdt` the rule is to make an arbitrary (but
/// deterministic) choice between the two. When using an `Option<T>` instead with this impl, ambiguity
/// cases are explicitly stored as None, which allows us to detect the ambiguity and handle it in
/// the way we want. (this can only work if we are happy with losing the value when an ambiguity
/// arises)
impl<T> Crdt for Option<T>
where
T: Eq,
{
fn merge(&mut self, other: &Self) {
if self != other {
*self = None;
}
}
}
/// All types that implement `Ord` (a total order) can also implement a trivial CRDT
/// defined by the merge rule: `a ⊔ b = max(a, b)`. Implement this trait for your type
/// to enable this behavior.
+2
View File
@@ -16,6 +16,7 @@ mod deletable;
mod lww;
mod lww_map;
mod map;
mod option;
pub use self::bool::*;
pub use crdt::*;
@@ -23,3 +24,4 @@ pub use deletable::*;
pub use lww::*;
pub use lww_map::*;
pub use map::*;
pub use option::*;
+97
View File
@@ -0,0 +1,97 @@
use serde::{Deserialize, Serialize};
use crate::crdt::Crdt;
#[derive(Serialize, Deserialize, Clone, Default, Copy, PartialEq, Eq, Debug)]
#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))]
#[serde(transparent)]
pub struct CancelingOption<T>(pub Option<T>);
/// `CancelingOption<T>` implements Crdt for any type T, even if T doesn't implement CRDT itself: when
/// different values are detected, they are always merged to None. This can be used for value
/// types which shoulnd't be merged, instead of trying to merge things when we know we don't want
/// to merge them (which is what the `AutoCrdt` trait is used for most of the time). This cases
/// arises very often, for example with a Lww or a `LwwMap`: the value type has to be a CRDT so that
/// we have a rule for what to do when timestamps aren't enough to disambiguate (in a distributed
/// system, anything can happen!), and with `AutoCrdt` the rule is to make an arbitrary (but
/// deterministic) choice between the two. When using an `CancelingOption<T>` instead with this impl, ambiguity
/// cases are explicitly stored as None, which allows us to detect the ambiguity and handle it in
/// the way we want. (this can only work if we are happy with losing the value when an ambiguity
/// arises)
impl<T> Crdt for CancelingOption<T>
where
T: Eq + Clone,
{
fn merge(&mut self, other: &Self) {
match (self.0.as_ref(), other.0.as_ref()) {
(Some(a), Some(b)) if a != b => {
self.0 = None;
}
(None, Some(b)) => {
self.0 = Some(b.clone());
}
_ => {}
}
}
}
impl<T> CancelingOption<T> {
pub fn inner(&self) -> Option<&T> {
self.0.as_ref()
}
pub fn into_inner(self) -> Option<T> {
self.0
}
pub fn map<U>(self, f: impl FnOnce(T) -> U) -> CancelingOption<U> {
CancelingOption(self.0.map(f))
}
}
impl<T> From<Option<T>> for CancelingOption<T> {
fn from(x: Option<T>) -> Self {
Self(x)
}
}
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, Copy, Default)]
#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))]
#[serde(transparent)]
pub struct MergingOption<T>(pub Option<T>);
/// `MergingOption<T>` implements `Crdt` when `T` implements `Crdt`:
/// None is a bottom value, and different Some values get merged according
/// to their Crdt operator.
impl<T> Crdt for MergingOption<T>
where
T: Crdt + Clone,
{
fn merge(&mut self, other: &Self) {
if let (Some(a), Some(b)) = (self.0.as_mut(), other.0.as_ref()) {
a.merge(b);
} else {
self.0 = self.0.take().or_else(|| other.0.clone());
}
}
}
impl<T> MergingOption<T> {
pub fn inner(&self) -> Option<&T> {
self.0.as_ref()
}
pub fn into_inner(self) -> Option<T> {
self.0
}
pub fn map<U>(self, f: impl FnOnce(T) -> U) -> MergingOption<U> {
MergingOption(self.0.map(f))
}
}
impl<T> From<Option<T>> for MergingOption<T> {
fn from(x: Option<T>) -> Self {
Self(x)
}
}