refactor!: K comes first in History, HistoryBuilder and Joint

`K` is the one type parameter people change, and it was last. Naming a
history in a struct field meant writing all four to say one thing:

    struct Ladder { history: History<i64, ConstantDrift, NullObserver, String> }
    struct Analysis<'h> { joint: Joint<'h, i64, ConstantDrift, NullObserver, &'static str> }

Now:

    struct Ladder { history: History<String> }
    struct Analysis<'h> { joint: Joint<'h> }

`History<K, T, D, O>`, all four defaulted. Bounds may reference later
parameters, so `D: Drift<T> = ConstantDrift` is legal in third position.
`Joint` gains the same defaults, so `Joint<'h, String>` spells it.

72 call sites swapped, and the reorder makes most of them shorter: 18
now read `History<String>` and the `&'static str` ones read `History`.
The two turbofished builders shrink from
`HistoryBuilder::<Untimed, _, _, String>::new()` to
`HistoryBuilder::<String, Untimed>::new()`.

`Joint` keeps `O` structurally, defaulted rather than removed. #72 notes
it never touches the observer, which is true — but it borrows the whole
`&'h History<K, T, D, O>` and calls `History::resolve_terms`, so dropping
the parameter means either a view type or moving that method off
`History`. The default already buys the entire user-visible benefit,
which was the spelling.

Refs #72.

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-10 06:49:22 +02:00
co-authored by Claude Opus 5
parent d2ab4446ef
commit b553c630f5
29 changed files with 129 additions and 125 deletions
+1 -1
View File
@@ -29,7 +29,7 @@ fn additive_structure_makes_sums_wide_and_differences_tight() {
let players = ["p0", "p1", "p2"];
let holes = ["h0", "h1"];
let mut h: History<i64, _, _, &'static str> = History::builder()
let mut h: History = History::builder()
.mu(0.0)
.sigma(6.0)
.beta(1.0)
+2 -2
View File
@@ -11,7 +11,7 @@ use trueskill_tt::{
ConstantDrift, ConvergenceOptions, Event, History, InferenceError, Member, Outcome, Team,
};
type H = History<i64, ConstantDrift, trueskill_tt::NullObserver, &'static str>;
type H = History;
fn duel(a: &'static str, b: &'static str, t: i64) -> Event<i64, &'static str> {
Event {
@@ -108,7 +108,7 @@ fn the_two_agree_on_a_converged_fit() {
/// At the old value of 30 this history stopped short and said nothing.
#[test]
fn the_default_cap_clears_an_ordinary_history() {
let mut h: History<i64, ConstantDrift, _, String> = History::builder()
let mut h: History<String> = History::builder()
.key_type::<String>()
.mu(0.0)
.sigma(6.0)
+2 -3
View File
@@ -15,8 +15,7 @@ use std::{env, process::Command};
use smallvec::smallvec;
use trueskill_tt::{
ConstantDrift, ConvergenceOptions, Event, History, Member, NullObserver, Outcome, Team,
UnknownKeys,
ConstantDrift, ConvergenceOptions, Event, History, Member, Outcome, Team, UnknownKeys,
};
/// Set in the child so it reports instead of re-spawning.
@@ -24,7 +23,7 @@ const CHILD: &str = "TSTT_DETERMINISM_CHILD";
const RUNS: usize = 40;
type H = History<i64, ConstantDrift, NullObserver, String>;
type H = History<String>;
fn fitted() -> H {
let mut h: H = History::builder()
+2 -2
View File
@@ -8,7 +8,7 @@ mod common;
use common::assert_finite;
use trueskill_tt::{
ConstantDrift, ConvergenceOptions, Game, GameOptions, Gaussian, History, InferenceError,
NullObserver, Outcome, Rating,
Outcome, Rating,
};
type R = Rating<i64, ConstantDrift>;
@@ -126,7 +126,7 @@ fn empty_history_converges_trivially() {
/// indexed out of bounds in release, so this must run in both profiles.
#[test]
fn converge_on_an_empty_history_with_owned_keys() {
let mut history: History<i64, ConstantDrift, NullObserver, String> = History::builder()
let mut history: History<String> = History::builder()
.key_type::<String>()
.score_sigma(5.0)
.build();
+3 -3
View File
@@ -8,11 +8,11 @@
use smallvec::smallvec;
use trueskill_tt::{
ConstantDrift, ConvergenceOptions, Event, Gaussian, History, InferenceError, Member,
NullObserver, Outcome, Team,
ConstantDrift, ConvergenceOptions, Event, Gaussian, History, InferenceError, Member, Outcome,
Team,
};
type Fit = History<i64, ConstantDrift, NullObserver, &'static str>;
type Fit = History;
const CONVERGENCE: ConvergenceOptions = ConvergenceOptions {
max_iter: 64,
+1 -1
View File
@@ -11,7 +11,7 @@ use trueskill_tt::{
Team,
};
type H = History<i64, ConstantDrift, trueskill_tt::NullObserver, &'static str>;
type H = History;
fn history() -> H {
History::builder()
+2 -4
View File
@@ -4,11 +4,9 @@
//! `filtered_log_evidence_for` was the missing corner: the one a per-competitor
//! prequential score needs.
use trueskill_tt::{
ConstantDrift, Event, History, InferenceError, Member, NullObserver, Outcome, Team,
};
use trueskill_tt::{Event, History, InferenceError, Member, Outcome, Team};
type H = History<i64, ConstantDrift, NullObserver, &'static str>;
type H = History;
/// Two disjoint cohorts, so a key restriction is guaranteed to leave events out.
fn two_cohorts() -> H {
+2 -4
View File
@@ -4,11 +4,9 @@
//! Each test carries a control: the same call on a key the history *does* know,
//! so it cannot pass merely because everything returns the same thing.
use trueskill_tt::{
ConstantDrift, Event, History, InferenceError, Member, NullObserver, Outcome, Team,
};
use trueskill_tt::{Event, History, InferenceError, Member, Outcome, Team};
type H = History<i64, ConstantDrift, NullObserver, &'static str>;
type H = History;
fn history() -> H {
let mut h = H::default();
+1 -1
View File
@@ -47,7 +47,7 @@ fn configured_event(a: &str, b: &str, time: i64, scale: f64) -> Event<i64, Strin
}
fn converged_skills(events: Vec<Event<i64, String>>, batched: bool) -> Vec<(String, Gaussian)> {
let mut h: History<i64, _, _, String> = History::builder()
let mut h: History<String> = History::builder()
.key_type::<String>()
.convergence(tight())
.build();
+1 -2
View File
@@ -14,8 +14,7 @@ use trueskill_tt::{Event, History, InferenceError, Member, Outcome, Team};
type Ev = Event<i64, &'static str>;
fn history() -> History<i64, trueskill_tt::ConstantDrift, trueskill_tt::NullObserver, &'static str>
{
fn history() -> History {
History::builder().score_sigma(1.0).build()
}
+2 -2
View File
@@ -11,7 +11,7 @@ use trueskill_tt::{
UnknownKeys,
};
type H = History<i64, ConstantDrift, trueskill_tt::NullObserver, &'static str>;
type H = History;
fn duel(a: &'static str, b: &'static str, t: i64, sa: f64, sb: f64) -> Event<i64, &'static str> {
Event {
@@ -288,7 +288,7 @@ fn unseen_competitors_match_a_fresh_factorisation() {
#[test]
fn a_drift_too_small_to_represent_collapses_rather_than_corrupting() {
fn variance(scale: f64) -> f64 {
let mut h: History<i64, ConstantDrift, _, String> = History::builder()
let mut h: History<String> = History::builder()
.key_type::<String>()
.mu(0.0)
.sigma(6.0)
+3 -3
View File
@@ -8,10 +8,10 @@
//! Both key types are exercised in every test, because the point is that the
//! spelling is the same.
use trueskill_tt::{ConstantDrift, History, NullObserver};
use trueskill_tt::{ConstantDrift, History};
type Owned = History<i64, ConstantDrift, NullObserver, String>;
type Borrowed = History<i64, ConstantDrift, NullObserver, &'static str>;
type Owned = History<String>;
type Borrowed = History;
fn owned() -> Owned {
let mut h: Owned = History::builder().key_type::<String>().build();
+2 -2
View File
@@ -3,7 +3,7 @@
//! produced a tiny-negative precision whose `sigma() = 1/sqrt(pi)` was NaN, which the
//! moment-space `Sub` in the game chain propagated into every skill once the slice grew past
//! ~75 competitors (e.g. a real ranking dataset with hundreds of players).
use trueskill_tt::{ConstantDrift, ConvergenceOptions, EPSILON, History, ITERATIONS, NullObserver};
use trueskill_tt::{ConstantDrift, ConvergenceOptions, EPSILON, History, ITERATIONS};
/// Tiny deterministic LCG — avoids a dev-dependency on `rand`.
struct Lcg(u64);
@@ -24,7 +24,7 @@ impl Lcg {
}
fn nan_after_fit(players: usize) -> usize {
let mut h: History<i64, ConstantDrift, NullObserver, String> = History::builder()
let mut h: History<String> = History::builder()
.key_type::<String>()
.beta(1.0)
.sigma(6.0)
+3 -5
View File
@@ -132,10 +132,8 @@ fn key(i: usize) -> &'static str {
}
/// Returns (worst mean error, worst sd ratio).
fn fitted(
obs: &[(usize, usize, f64)],
) -> History<i64, ConstantDrift, trueskill_tt::NullObserver, &'static str> {
let mut h: History<i64, _, _, &'static str> = History::builder()
fn fitted(obs: &[(usize, usize, f64)]) -> History {
let mut h: History = History::builder()
.mu(MU0)
.sigma(SIGMA0)
.beta(BETA)
@@ -324,7 +322,7 @@ fn cost_scaling() {
use std::time::Instant;
for n in [50usize, 100, 200, 400, 800] {
let names: Vec<String> = (0..n).map(|i| format!("c{i}")).collect();
let mut h: History<i64, _, _, String> = History::builder()
let mut h: History<String> = History::builder()
.key_type::<String>()
.score_sigma(2.0)
.drift(ConstantDrift::new(0.0))
+2 -6
View File
@@ -6,9 +6,7 @@ use trueskill_tt::{
UnknownKeys,
};
fn builder(
policy: UnknownKeys,
) -> History<i64, ConstantDrift, trueskill_tt::NullObserver, &'static str> {
fn builder(policy: UnknownKeys) -> History {
History::builder()
.mu(0.0)
.sigma(6.0)
@@ -37,9 +35,7 @@ fn round(a: &'static str, b: &'static str, sa: f64, sb: f64) -> Event<i64, &'sta
/// A history where "veteran" and "regular" are well observed and "novice"
/// appears once.
fn fitted(
policy: UnknownKeys,
) -> History<i64, ConstantDrift, trueskill_tt::NullObserver, &'static str> {
fn fitted(policy: UnknownKeys) -> History {
let mut h = builder(policy);
let mut events: Vec<_> = (0..40)
.map(|t| round("veteran", "regular", 10.0 + f64::from(t % 3), 5.0))
+2 -2
View File
@@ -10,10 +10,10 @@
//! returning `Err`.
use trueskill_tt::{
ConstantDrift, Event, Gaussian, History, InferenceError, Member, NullObserver, Outcome, Team,
ConstantDrift, Event, Gaussian, History, InferenceError, Member, Outcome, Team,
};
type H = History<i64, ConstantDrift, NullObserver, &'static str>;
type H = History;
fn build(beta: f64, prior: Option<Gaussian>, outcome: Outcome) -> H {
let mut h: H = History::builder()
+2 -2
View File
@@ -35,7 +35,7 @@ fn ev(a: &str, b: &str, time: i64) -> Event<i64, String> {
/// Ingest each chunk in turn, converging fully after every one.
fn fit_in_chunks(chunks: Vec<Events>) -> Vec<(String, Gaussian)> {
let mut h: History<i64, _, _, String> = History::builder()
let mut h: History<String> = History::builder()
.key_type::<String>()
.convergence(tight())
.build();
@@ -152,7 +152,7 @@ fn re_converging_an_unchanged_history_costs_one_iteration() {
let (early, late) = fixture();
let all: Vec<_> = early.into_iter().chain(late).collect();
let mut h: History<i64, _, _, String> = History::builder()
let mut h: History<String> = History::builder()
.key_type::<String>()
.convergence(tight())
.build();
+1 -1
View File
@@ -11,7 +11,7 @@ use trueskill_tt::{
Team,
};
type H = History<i64, ConstantDrift, trueskill_tt::NullObserver, &'static str>;
type H = History;
const PINNED: Gaussian = Gaussian::from_ms(2.0, 0.5);
+1 -1
View File
@@ -144,7 +144,7 @@ fn key_type_replaces_builder_with_key() {
/// Both axes at once, via the explicit constructor rather than the setters.
#[test]
fn new_constructs_on_any_axis_directly() {
let mut h = HistoryBuilder::<Season, _, _, String>::new().build();
let mut h = HistoryBuilder::<String, Season>::new().build();
h.record_winner(&"a".to_string(), &"b".to_string(), Season(7))
.unwrap();
assert!(h.converge().unwrap().converged);
+1 -1
View File
@@ -16,7 +16,7 @@ const BETA: f64 = 1.0;
const SCORE_SIGMA: f64 = 2.0;
const GAMMA: f64 = 0.5;
type H = History<i64, ConstantDrift, trueskill_tt::NullObserver, &'static str>;
type H = History;
fn history(gamma: f64) -> H {
History::builder()
+1 -1
View File
@@ -42,7 +42,7 @@ fn a_struct_holding_a_history_can_derive_debug() {
#[test]
fn history_builder_is_debug_and_clone() {
let b: HistoryBuilder<i64, ConstantDrift, _, &'static str> = History::builder();
let b: HistoryBuilder = History::builder();
let cloned = b.clone();
assert!(!format!("{cloned:?}").is_empty());
}
+2 -2
View File
@@ -6,7 +6,7 @@ use trueskill_tt::{
UnknownKeys,
};
type H = History<i64, ConstantDrift, trueskill_tt::NullObserver, &'static str>;
type H = History;
fn round(a: &'static str, b: &'static str, sa: f64, sb: f64) -> Event<i64, &'static str> {
Event {
@@ -30,7 +30,7 @@ fn base() -> Vec<Event<i64, &'static str>> {
}
fn fit(extra: Option<Event<i64, &'static str>>, policy: UnknownKeys) -> H {
let mut h: History<i64, _, _, &'static str> = History::builder()
let mut h: History = History::builder()
.mu(0.0)
.sigma(6.0)
.beta(1.0)