feat: add the missing trait impls and make #[must_use] consistent

Trait coverage (#76), all additive:

  History          Debug is still absent - see below
  HistoryBuilder   + Debug   (it derived Clone but not Debug)
  Rating           + PartialEq  (Gaussian had it; Rating is a Gaussian
                                 plus three scalars and had none)
  Event/Team/Member + PartialEq (input value types with no way to compare
                                 them, which made round-trip tests awkward)
  ConvergenceReport + PartialEq

`#[must_use]` (#67). The coverage had no rule: `filtered_log_evidence`
had it and `log_evidence` did not; `rating` had it and `current_skill`
did not; `Rating::with_drift_scale` had it and `Member::with_drift_scale`
did not.

Now on the types — `EventBuilder`, `HistoryBuilder`, `Prediction`,
`Gaussian`, `OwnedGame` — which covers most method returns at once, plus
the `History` accessors individually.

`EventBuilder` gets a message, because a dropped builder is the worst
case in the set: measured, `h.event(1).team(["x"]).team(["y"]).winner(0)`
without `.commit()` leaves `time_slices_len() == 0` and every skill
`None`, with no warning at all.

And `ConvergenceReport`'s `#[must_use]` moves off the TYPE onto
`converge_partial`, where its stated reason is true. It read "from
`converge_partial` this may describe a fit that stopped at max_iter" but
fired on `converge` too — where that is false, since `converge` returns
`Err(NotConverged)` in exactly that case. So the crate's own front-page
example warned, and every quickstart had to write `let _ =`. Verified
from a consumer crate: `h.converge()?;` now compiles clean.

Marking the types made eight method-level attributes redundant, which
clippy's `double_must_use` caught — that is the type-level marker doing
its job, and the eight are removed.

Refs #76, #67

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011hcFjNDmHXZF8URGLku5zZ
This commit is contained in:
2026-09-09 20:38:39 +02:00
co-authored by Claude Opus 5
parent 4472d98b56
commit a0c2f78aed
11 changed files with 27 additions and 24 deletions
+13 -5
View File
@@ -24,7 +24,8 @@ use crate::{
tuple_gt, tuple_max,
};
#[derive(Clone)]
#[derive(Clone, Debug)]
#[must_use = "a builder does nothing until `.build()`"]
pub struct HistoryBuilder<
T: Time = i64,
D: Drift<T> = ConstantDrift,
@@ -197,7 +198,6 @@ impl<T: Time, D: Drift<T>, O: Observer<T>, K: Eq + Hash + Clone> HistoryBuilder<
/// assert!(h.current_skill(&"alice").unwrap().mu() > 0.0);
/// # Ok::<(), trueskill_tt::InferenceError>(())
/// ```
#[must_use]
pub fn time_type<T2>(self) -> HistoryBuilder<T2, D, O, K>
where
T2: Time,
@@ -232,7 +232,6 @@ impl<T: Time, D: Drift<T>, O: Observer<T>, K: Eq + Hash + Clone> HistoryBuilder<
/// h.record_winner(&"alice".to_string(), &"bob".to_string(), 1)?;
/// # Ok::<(), trueskill_tt::InferenceError>(())
/// ```
#[must_use]
pub fn key_type<K2: Eq + Hash + Clone>(self) -> HistoryBuilder<T, D, O, K2> {
HistoryBuilder {
mu: self.mu,
@@ -418,7 +417,6 @@ impl Default for History<i64, ConstantDrift, NullObserver, &'static str> {
}
impl History<i64, ConstantDrift, NullObserver, &'static str> {
#[must_use]
pub fn builder() -> HistoryBuilder<i64, ConstantDrift, NullObserver, &'static str> {
HistoryBuilder::default()
}
@@ -440,7 +438,6 @@ impl<T: Time, K: Eq + Hash + Clone> HistoryBuilder<T, ConstantDrift, NullObserve
/// h.converge()?;
/// # Ok::<(), trueskill_tt::InferenceError>(())
/// ```
#[must_use]
pub fn new() -> Self {
Self::default()
}
@@ -455,6 +452,7 @@ impl<T: Time, D: Drift<T>, O: Observer<T>, K: Eq + Hash + Clone> History<T, D, O
self.keys.get_or_create(key)
}
#[must_use]
pub fn lookup<Q>(&self, key: &Q) -> Option<Index>
where
K: Borrow<Q>,
@@ -563,6 +561,7 @@ impl<T: Time, D: Drift<T>, O: Observer<T>, K: Eq + Hash + Clone> History<T, D, O
/// assert_eq!(names, ["alice", "bob"]);
/// # Ok::<(), trueskill_tt::InferenceError>(())
/// ```
#[must_use]
pub fn competitors(&self) -> impl ExactSizeIterator<Item = &K> {
self.keys.keys()
}
@@ -580,6 +579,7 @@ impl<T: Time, D: Drift<T>, O: Observer<T>, K: Eq + Hash + Clone> History<T, D, O
}
/// Learning curves for all competitors, keyed by their user-facing key.
#[must_use]
pub fn learning_curves(&self) -> HashMap<K, Vec<(T, Gaussian)>> {
#[cfg(feature = "rayon")]
{
@@ -728,6 +728,7 @@ impl<T: Time, D: Drift<T>, O: Observer<T>, K: Eq + Hash + Clone> History<T, D, O
self.agents.contains(idx).then(|| self.agents[idx].rating)
}
#[must_use]
pub fn current_skill<Q>(&self, key: &Q) -> Option<Gaussian>
where
K: std::borrow::Borrow<Q>,
@@ -741,6 +742,7 @@ impl<T: Time, D: Drift<T>, O: Observer<T>, K: Eq + Hash + Clone> History<T, D, O
}
/// Learning curve for a single key: (time, posterior) pairs in time order.
#[must_use]
pub fn learning_curve<Q>(&self, key: &Q) -> Vec<(T, Gaussian)>
where
K: std::borrow::Borrow<Q>,
@@ -764,6 +766,7 @@ impl<T: Time, D: Drift<T>, O: Observer<T>, K: Eq + Hash + Clone> History<T, D, O
/// Runs a full forward pass per call and caches nothing. This is the
/// entry point for multi-key work — see `filtered_learning_curve` for
/// why calling that once per key is far more expensive.
#[must_use]
pub fn filtered_learning_curves(&self) -> HashMap<K, Vec<(T, Gaussian)>> {
let mut data: HashMap<K, Vec<(T, Gaussian)>> = HashMap::new();
@@ -786,6 +789,7 @@ impl<T: Time, D: Drift<T>, O: Observer<T>, K: Eq + Hash + Clone> History<T, D, O
/// discarding every posterior but the requested key's. N keys fetched
/// this way costs O(N * events); use `filtered_learning_curves` for
/// multi-key work instead — it computes the same pass once.
#[must_use]
pub fn filtered_learning_curve<Q>(&self, key: &Q) -> Vec<(T, Gaussian)>
where
K: Borrow<Q>,
@@ -841,12 +845,14 @@ impl<T: Time, D: Drift<T>, O: Observer<T>, K: Eq + Hash + Clone> History<T, D, O
}
/// Total log-evidence across the history.
#[must_use]
pub fn log_evidence(&self) -> f64 {
self.log_evidence_internal(false, &[])
}
/// Log-evidence restricted to time slices containing at least one of the
/// given keys. Useful for leave-one-out cross-validation.
#[must_use]
pub fn log_evidence_for<Q>(&self, keys: &[&Q]) -> f64
where
K: std::borrow::Borrow<Q>,
@@ -1759,6 +1765,8 @@ impl<T: Time, D: Drift<T>, O: Observer<T>, K: Eq + Hash + Clone> History<T, D, O
/// # Errors
///
/// `NonFiniteResult` if a sweep produces a NaN or infinite step.
#[must_use = "this fit may have stopped at `max_iter` — check `converged`, \
or bind it to `_` to say you have decided not to"]
pub fn converge_partial(&mut self) -> Result<ConvergenceReport, InferenceError> {
use std::time::Instant;