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:
+13
-5
@@ -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;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user