pub const ACC_BYTES: usize = 64;
pub trait Monoid: Copy + PartialEq + core::fmt::Debug {
const IDENTITY: Self;
fn combine(self, other: Self) -> Self;
fn to_acc_bytes(&self) -> [u8; ACC_BYTES];
fn from_acc_bytes(b: &[u8; ACC_BYTES]) -> Self;
}
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub struct Sum(pub u64);
impl Monoid for Sum {
const IDENTITY: Self = Sum(0);
fn combine(self, other: Self) -> Self {
Sum(self.0.saturating_add(other.0))
}
fn to_acc_bytes(&self) -> [u8; ACC_BYTES] {
let mut out = [0u8; ACC_BYTES];
out[0..8].copy_from_slice(&self.0.to_le_bytes());
out
}
fn from_acc_bytes(b: &[u8; ACC_BYTES]) -> Self {
let mut w = [0u8; 8];
w.copy_from_slice(&b[0..8]);
Sum(u64::from_le_bytes(w))
}
}
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub struct MaxWinner {
pub score: u64,
pub winner: u64,
}
impl Monoid for MaxWinner {
const IDENTITY: Self = MaxWinner {
score: 0,
winner: u64::MAX,
};
fn combine(self, other: Self) -> Self {
if other.score > self.score {
other
} else if other.score < self.score {
self
} else if other.winner < self.winner {
other } else {
self
}
}
fn to_acc_bytes(&self) -> [u8; ACC_BYTES] {
let mut out = [0u8; ACC_BYTES];
out[0..8].copy_from_slice(&self.score.to_le_bytes());
out[8..16].copy_from_slice(&self.winner.to_le_bytes());
out
}
fn from_acc_bytes(b: &[u8; ACC_BYTES]) -> Self {
let mut s = [0u8; 8];
let mut w = [0u8; 8];
s.copy_from_slice(&b[0..8]);
w.copy_from_slice(&b[8..16]);
MaxWinner {
score: u64::from_le_bytes(s),
winner: u64::from_le_bytes(w),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
fn check_laws<M: Monoid>(samples: &[M]) {
for &a in samples {
assert_eq!(a.combine(M::IDENTITY), a, "right identity for {a:?}");
assert_eq!(M::IDENTITY.combine(a), a, "left identity for {a:?}");
for &b in samples {
assert_eq!(a.combine(b), b.combine(a), "commutativity {a:?} {b:?}");
for &c in samples {
assert_eq!(
a.combine(b).combine(c),
a.combine(b.combine(c)),
"associativity {a:?} {b:?} {c:?}"
);
}
}
}
}
fn assert_acc_roundtrip<M: Monoid>(v: M) {
assert_eq!(
M::from_acc_bytes(&v.to_acc_bytes()),
v,
"acc-bytes round-trip {v:?}"
);
}
#[test]
fn identity_and_values_roundtrip_through_acc_bytes() {
assert_acc_roundtrip(Sum::IDENTITY);
assert_acc_roundtrip(Sum(123_456_789));
assert_acc_roundtrip(MaxWinner::IDENTITY);
assert_acc_roundtrip(MaxWinner {
score: 99,
winner: 7,
});
assert_eq!(MaxWinner::IDENTITY.winner, u64::MAX);
assert_ne!(MaxWinner::IDENTITY.to_acc_bytes(), [0u8; ACC_BYTES]);
}
#[test]
fn sum_is_a_monoid() {
let s: Vec<Sum> = [0u64, 1, 7, 42, 1000].iter().map(|&x| Sum(x)).collect();
check_laws(&s);
assert_eq!(Sum(3).combine(Sum(4)), Sum(7));
}
#[test]
fn sum_saturates() {
assert_eq!(Sum(u64::MAX).combine(Sum(5)), Sum(u64::MAX));
}
#[test]
fn maxwinner_is_a_monoid() {
let s: Vec<MaxWinner> = [(0u64, 9u64), (5, 1), (5, 3), (8, 2), (8, 0)]
.iter()
.map(|&(score, winner)| MaxWinner { score, winner })
.collect();
check_laws(&s);
}
#[test]
fn reduce_is_invariant_under_shard_count() {
use crate::partition::{partition_len, partition_start};
let scores = [3u64, 0, 9, 9, 1, 0, 5, 9, 2, 0];
let total = scores.len() as u64;
let member = |i: u64| MaxWinner {
score: scores[i as usize],
winner: i,
};
let mut single = MaxWinner::IDENTITY;
for i in 0..total {
single = single.combine(member(i));
}
assert_eq!(
single,
MaxWinner {
score: 9,
winner: 2
},
"max 9, lowest tied index 2"
);
for p in 1..=total {
let mut acc = MaxWinner::IDENTITY; for sid in 0..p {
let start = partition_start(total, p, sid);
let len = partition_len(total, p, sid);
let mut partial = MaxWinner::IDENTITY; for j in 0..len {
partial = partial.combine(member(start + j));
}
acc = acc.combine(partial);
}
assert_eq!(acc, single, "REDUCE result must be invariant under P={p}");
}
}
#[test]
fn maxwinner_picks_higher_score_then_lower_index() {
let a = MaxWinner {
score: 5,
winner: 3,
};
let b = MaxWinner {
score: 8,
winner: 7,
};
assert_eq!(a.combine(b), b, "higher score wins");
let c = MaxWinner {
score: 8,
winner: 2,
};
assert_eq!(b.combine(c), c, "tie -> lower index wins");
}
}