Twenty-one breaking changes in one release, with a live consumer. The
changelog lists them; `MIGRATING.md` says what to do about them, leading
with the three that change what an existing, *compiling* call returns —
unknown keys, predictions from a broken fit, and `Gaussian`'s operators
— since those are the ones the compiler will not find for you.
Every "after" snippet was compiled, not written from memory, and doing
so caught three errors in my own guide:
- `log_evidence_for(&[&"alice"])` does not compile at `K = String`. The
right spelling is `&["alice"]`, which works at *both* key types —
checked, because a guide that is right for half its readers is worse
than no guide.
- the same for `filtered_log_evidence_for`
- the `Analysis<'h> { joint: Joint<'h> }` example needs a history at the
default key type; pairing it with a `History<String>` does not compile
git-cliff skips merge commits now. Every branch lands with `--no-ff`, so
a release's merges outnumber its real commits and say nothing the merged
ones do not — 0.9.0's changelog had fourteen lines of them under "Other
(unconventional)". `ci:` commits get a group instead of falling through
to that catch-all.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011hcFjNDmHXZF8URGLku5zZ
7.9 KiB
Migrating
0.8.0 → 0.9.0
Twenty-one breaking changes. Nearly all of them are mechanical, and the compiler finds every one — nothing here changes behaviour silently.
Three exceptions are worth reading before you start, because they change
what an existing, compiling call returns: unknown keys,
predictions from a broken fit,
and Gaussian's operators.
Type parameters: K comes first
K was last, so naming a history meant writing all four parameters to change
the one that matters.
// before
struct Ladder { history: History<i64, ConstantDrift, NullObserver, String> }
struct Analysis<'h> { joint: Joint<'h, i64, ConstantDrift, NullObserver, &'static str> }
// after
struct Ladder { history: History<String> }
struct Analysis<'h> { joint: Joint<'h> }
History<K, T, D, O, R> — key, time, drift, observer, rating rule — all
defaulted. HistoryBuilder matches. There is a fifth parameter now (R), and
you will never write it unless you use default_rating_for.
HistoryBuilder::<Untimed, _, _, String>::new() becomes
HistoryBuilder::<String, Untimed>::new().
Predictions and joint queries take borrowed keys
At K = String a string literal used to be impossible, and asking "who wins"
cost four allocations of temporaries that all had to outlive the call.
// before, at K = String
let ta = vec![a.to_string()];
let ra: Vec<&String> = ta.iter().collect();
let tb = vec![b.to_string()];
let rb: Vec<&String> = tb.iter().collect();
let teams: Vec<&[&String]> = vec![&ra, &rb];
h.predict_win_probabilities(&teams)?;
// after, at either key type
h.predict_win_probabilities(&[&["alice"], &["bob"]])?;
h.posterior_of(&[("alice", 1.0), ("bob", -1.0)])?;
At K = &'static str the old &[&[&"a"]] spelling still compiles — Q infers
to &str and the two shapes coincide — so this is only a break for owned keys,
where nothing compiled before.
One cost: predict_outcome(&[]) can no longer infer the key type. Annotate it,
let none: &[&[&str]] = &[];. It bites only on that degenerate call.
Unknown keys are reported, not skipped
Read this one. log_evidence_for used to filter_map unknown keys away,
and an empty target list means no restriction downstream — so a list of
entirely unknown keys returned the whole-history value. Measured:
log_evidence_for(["typo"]) returned exactly log_evidence(). On the one
workload it is documented for, leave-one-out cross-validation, that is the
un-held-out score.
let e = h.log_evidence_for(&["alice"])?; // now Result
let curve = h.learning_curve("alice"); // now Option
Note &["alice"], not &[&"alice"]. These take borrowed keys like the
prediction methods, so one spelling works at both key types.
learning_curve and filtered_learning_curve return Option: None is "never
heard of this key", Some(vec![]) is "known, has not played". They used to be
the same empty Vec.
Predictions refuse a fit they cannot answer from
Read this one too. converge already refused to report a NaN fit, but
nothing stopped a caller ignoring that error and predicting anyway. On a
NaN-poisoned fit, quality returned Ok(NaN), predict_outcome().total() was
NaN, and predict_win_probabilities returned Ok([0.0, 0.0]) — finite,
plausible, and summing to zero against a doc promising one.
Every predict_* path now returns Err(NonFiniteSkill { .. }) there, and
Err(NoPerformanceVariance) when beta is zero and every skill is a point
mass. If you were ignoring converge's error, you will start seeing these.
Gaussian's operators are gone
Mul, Div, Add and Sub were the EP product, cavity and variance-space
convolutions, not arithmetic — N(10,2) * N(4,3) is N(8.15, 1.66), and
a / c could leave a negative precision whose mu() printed a confident 0.
They are pub(crate) inherent methods now. The public surface is from_ms,
from_mv, mu, sigma, variance, probability_below, probability_above;
pi() and tau() are internal. If you compared fits bit-for-bit on
(pi, tau), compare (mu, variance) — same information, still exact.
Game is the type you get
Game::ranked returned an OwnedGame, so let g: Game = Game::ranked(..)? did
not compile. Names swapped: Game<T, D> is public, OwnedGame is gone.
one_v_one returns a Game rather than (Gaussian, Gaussian), so it can be
asked for log_evidence() like its siblings. For the old shape:
let post = Game::one_v_one(&a, &b, outcome, &opts)?.posteriors();
let (a_post, b_post) = (post[0][0], post[1][0]);
The joint is reached through Joint
History::posterior_of, posterior_of_at and expected_variance_reduction
were one-shot wrappers that re-factorised on every call. They are gone.
// before — pays for the factorisation twice
let a = h.posterior_of(&terms)?;
let b = h.posterior_of(&other)?;
// after — pays once, and the borrow says so
let joint = h.joint()?;
let a = joint.posterior_of(&terms)?;
let b = joint.posterior_of(&other)?;
InferenceError is typed
Six variants carried &'static str discriminators. Four enums replace them:
Parameter, Shape, OutcomeKind, CompetitorField.
// before
InferenceError::InvalidParameter { name: "drift_scale", value }
InferenceError::MismatchedShape { kind: "ranks vs teams", .. }
InferenceError::WrongOutcomeKind { context, expected, got } // three &str
// after
InferenceError::InvalidParameter { parameter: Parameter::DriftScale, value }
InferenceError::MismatchedShape { shape: Shape::OutcomeVsTeams, .. }
InferenceError::WrongOutcomeKind { expected: OutcomeKind::Ranked, got }
Variants that split or merged:
| before | after |
|---|---|
InvalidProbability { value } |
InvalidParameter { parameter: Parameter::PDraw, value } |
JointUnavailable { reason } |
EmptyHistory, JointRequiresScoredEvents, NotPositiveDefinite |
NonFiniteResult { context, step } |
NonFiniteStep { context, step } (convergence), NonFiniteSkill { mu, sigma } (prediction) |
Every struct variant is #[non_exhaustive], so match with a .. and
construct through the library.
Renames
| before | after |
|---|---|
History::predict_quality |
History::quality |
Outcome::scores_with_sigma |
Outcome::scores_with_noise |
EventBuilder::scores_with_sigma |
EventBuilder::scores_with_noise |
Outcome::Scored { sigma } |
Outcome::Scored { score_sigma } |
OwnedGame |
Game |
Removed
History::intern, History::lookup and Index. Nothing public ever accepted
an Index, so there was nothing to do with one. current_skill, rating and
learning_curve answer "does this history know this key" and all take a
borrowed key.
TimeSlice, EventKind, KeyTable, CompetitorStore, Competitor and N01
are no longer exported. None was obtainable from a History.
Warnings, not errors
#[must_use] now sits on the value types, so a dropped EventBuilder — an
event you forgot to .commit(), previously a silent no-op — warns. So do
dropped Team, Member, Outcome and Joint values. A -D warnings build
will need updating.
Nothing to do, but worth knowing
The joint factorisation is sparse with an AMD fill-reducing ordering:
745 ms → 1.11 ms on a 1976-appearance fixture, and near-linear scaling where
it was cubic. Results are unchanged; feral-amd is a new dependency (two
crates, both #![forbid(unsafe_code)]).
HistoryBuilder::gamma(f64) is shorthand for
.drift(ConstantDrift::new(gamma)).
History::current_skills() is the leaderboard query — every competitor's latest
posterior in one pass, rather than a full smoothed curve each.
History::filtered_log_evidence_for(&["alice"]) completes the evidence matrix:
forward-only and key-restricted, which is what per-competitor prequential
scoring needs.