`History` takes its observer by value and never hands it back, so a
caller who wanted to read what an observer recorded had no way to keep a
handle to it. The natural spelling did not compile:
let recorder = Arc::new(Recorder::default());
History::builder().observer(Arc::clone(&recorder))
// error[E0277]: `Arc<Recorder>: Observer<i64>` is not satisfied
The workaround was for every observer to wrap each of its own fields in
an `Arc` and derive `Clone` — one allocation and one lock per field, a
pattern each implementor had to rediscover, and nothing documenting it.
Adds blanket `Observer` impls for `Arc<O>`, `Box<O>` and `&O`. All are
`?Sized`, so `Arc<dyn Observer<T>>` and `Box<dyn Observer<T>>` work too
and an observer can be chosen at runtime. Also adds
`History::observer()` and `into_observer()`, so a non-shared observer's
state can be inspected in place or reclaimed after `converge` without
needing interior mutability at all.
`tests/observer.rs` is simplified to the shared spelling, so the
recommended pattern is the one demonstrated rather than the workaround.
Closes #40
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011hcFjNDmHXZF8URGLku5zZ
131 lines
4.7 KiB
Rust
131 lines
4.7 KiB
Rust
//! Observer trait for progress reporting during convergence.
|
|
//!
|
|
//! Replaces the old `verbose: bool` + `println!` path. Callers wire in any
|
|
//! observer that implements the trait; default methods are no-ops so users
|
|
//! override only what they need.
|
|
|
|
use crate::time::Time;
|
|
|
|
/// Receives progress callbacks during `History::converge`.
|
|
///
|
|
/// All methods have default no-op implementations; implement only what's
|
|
/// interesting.
|
|
pub trait Observer<T: Time>: Send + Sync {
|
|
/// Called after each convergence iteration across the whole history.
|
|
fn on_iteration_end(&self, _iter: usize, _max_step: (f64, f64)) {}
|
|
|
|
/// Called after each time slice is swept within an iteration.
|
|
///
|
|
/// A convergence iteration sweeps every slice twice — once travelling
|
|
/// backward through the history and once forward — so a multi-slice
|
|
/// history fires this twice per slice per iteration. A single-slice
|
|
/// history is swept once and fires once.
|
|
fn on_slice_processed(&self, _time: &T, _slice_idx: usize, _n_events: usize) {}
|
|
|
|
/// Called once when convergence completes (or max iters is reached).
|
|
fn on_converged(&self, _iters: usize, _final_step: (f64, f64), _converged: bool) {}
|
|
}
|
|
|
|
/// Shared and boxed observers forward to what they point at.
|
|
///
|
|
/// `History` takes its observer by value, so a caller who wants to *read* what
|
|
/// an observer recorded has to keep a handle to it. Without these impls the
|
|
/// natural spelling does not compile:
|
|
///
|
|
/// ```
|
|
/// # use std::sync::{Arc, Mutex};
|
|
/// # use trueskill_tt::{History, Observer};
|
|
/// #[derive(Default)]
|
|
/// struct Recorder {
|
|
/// iterations: Mutex<Vec<usize>>,
|
|
/// }
|
|
///
|
|
/// impl Observer<i64> for Recorder {
|
|
/// fn on_iteration_end(&self, iter: usize, _step: (f64, f64)) {
|
|
/// self.iterations.lock().unwrap().push(iter);
|
|
/// }
|
|
/// }
|
|
///
|
|
/// let recorder = Arc::new(Recorder::default());
|
|
/// let mut h = History::builder().observer(Arc::clone(&recorder)).build();
|
|
/// h.record_winner(&"a", &"b", 1).unwrap();
|
|
/// h.converge().unwrap();
|
|
///
|
|
/// // The caller's handle sees what the history's copy recorded.
|
|
/// assert!(!recorder.iterations.lock().unwrap().is_empty());
|
|
/// ```
|
|
///
|
|
/// The alternative was for every observer to wrap each of its own fields in an
|
|
/// `Arc` and derive `Clone` — one allocation and one lock per field, and a
|
|
/// pattern each implementor had to rediscover.
|
|
///
|
|
/// `?Sized` is deliberate: it makes `Arc<dyn Observer<T>>` and
|
|
/// `Box<dyn Observer<T>>` work, so observers can be chosen at runtime.
|
|
impl<T: Time, O: Observer<T> + ?Sized> Observer<T> for std::sync::Arc<O> {
|
|
fn on_iteration_end(&self, iter: usize, max_step: (f64, f64)) {
|
|
(**self).on_iteration_end(iter, max_step);
|
|
}
|
|
|
|
fn on_slice_processed(&self, time: &T, slice_idx: usize, n_events: usize) {
|
|
(**self).on_slice_processed(time, slice_idx, n_events);
|
|
}
|
|
|
|
fn on_converged(&self, iters: usize, final_step: (f64, f64), converged: bool) {
|
|
(**self).on_converged(iters, final_step, converged);
|
|
}
|
|
}
|
|
|
|
impl<T: Time, O: Observer<T> + ?Sized> Observer<T> for Box<O> {
|
|
fn on_iteration_end(&self, iter: usize, max_step: (f64, f64)) {
|
|
(**self).on_iteration_end(iter, max_step);
|
|
}
|
|
|
|
fn on_slice_processed(&self, time: &T, slice_idx: usize, n_events: usize) {
|
|
(**self).on_slice_processed(time, slice_idx, n_events);
|
|
}
|
|
|
|
fn on_converged(&self, iters: usize, final_step: (f64, f64), converged: bool) {
|
|
(**self).on_converged(iters, final_step, converged);
|
|
}
|
|
}
|
|
|
|
impl<T: Time, O: Observer<T> + ?Sized> Observer<T> for &O {
|
|
fn on_iteration_end(&self, iter: usize, max_step: (f64, f64)) {
|
|
(**self).on_iteration_end(iter, max_step);
|
|
}
|
|
|
|
fn on_slice_processed(&self, time: &T, slice_idx: usize, n_events: usize) {
|
|
(**self).on_slice_processed(time, slice_idx, n_events);
|
|
}
|
|
|
|
fn on_converged(&self, iters: usize, final_step: (f64, f64), converged: bool) {
|
|
(**self).on_converged(iters, final_step, converged);
|
|
}
|
|
}
|
|
|
|
/// ZST no-op observer; the default when none is configured.
|
|
#[derive(Copy, Clone, Debug, Default)]
|
|
pub struct NullObserver;
|
|
|
|
impl<T: Time> Observer<T> for NullObserver {}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
#[test]
|
|
fn null_observer_compiles_for_i64() {
|
|
let o = NullObserver;
|
|
<NullObserver as Observer<i64>>::on_iteration_end(&o, 1, (0.0, 0.0));
|
|
<NullObserver as Observer<i64>>::on_slice_processed(&o, &7, 0, 3);
|
|
<NullObserver as Observer<i64>>::on_converged(&o, 5, (1e-6, 1e-6), true);
|
|
}
|
|
|
|
#[test]
|
|
fn null_observer_compiles_for_untimed() {
|
|
use crate::Untimed;
|
|
let o = NullObserver;
|
|
<NullObserver as Observer<Untimed>>::on_iteration_end(&o, 1, (0.0, 0.0));
|
|
}
|
|
}
|