Two changes would take the realistic "fit, query, predict, store" program from unusable-without-reading-source to obvious. Both were prototyped and compile.
error[E0277]: the trait bound `&str: Borrow<String>` is not satisfied
error[E0271]: type mismatch resolving `<String as ToOwned>::Owned == &str`
Nothing there names the cause: History::default() silently pinned K = &'static str. The fix, after History::builder_with_key::<String>() also fails (E0107 — K is on the impl, not the fn, so it cannot be turbofished), is:
Four parameters named, three of them defaults, to change the one that matters. Reordering K first was prototyped and compiles — bounds may reference later parameters, so D: Drift<T> = ConstantDrift is legal in third position:
pubstructHistory<K: Eq+Hash+Clone=&'staticstr,T: Time=i64,D: Drift<T>=ConstantDrift,O: Observer<T>=NullObserver,>{..}structLadder{history: History<String>}// the whole spelling
Pair it with a turbofishable setter, mirroring the existing type-changing drift/observer:
That retires builder_with_key (see also #68, which needs the same treatment for T).
Also: Joint carries O but never uses it — nothing in its body touches the observer; it rides along inside &'h History. With the reorder, Joint<'h, K> is the whole spelling.
2. &[&[&K]] is the worst shape in the API, and it is a third vocabulary
The crate has four ways to say "some teams of competitors": typed Event/Team/Member, &[&[&K]] (all seven predict_*/expected_*), &[&[Rating]] (Game, free expected_information_gain), and &[&[Gaussian]] (quality).
At the default key type the guessed spelling fails:
h.predict_win_probabilities(&[&["alice","bob"]])// error[E0308]: expected `&&str`, found `&str`
At K = String — the realistic case — string literals are impossible and you must build three levels of temporaries that all outlive the call. This is the real code required to ask "who wins":
Six lines and four allocations. The same roster said the ingestion way is .team(names) — one expression.
This costs nothing to fix.member_skills and resolve_terms do exactly two things with a key: self.keys.get(*key) and format!("{key:?}"). Neither needs K itself:
Prototyped and run. It gives the same natural spelling for both key types:
h.predict_win_probabilities(&[&["alice"],&["bob"]])// K = String ✔
h.predict_win_probabilities(&[&["alice"],&["bob"]])// K = &'static str ✔
h.posterior_of(&[("alice",1.0),("bob",-1.0)])// no `&` on keys
A Vec<String> roster then needs one .map(String::as_str) instead of three nested collects. Bonus: K: Debug becomes Q: Debug, so the key type no longer has to be Debug to run a prediction.
3. lookup's bound is copy-paste and strictly too narrow
current_skill, rating, learning_curve, filtered_learning_curve and log_evidence_for all accept h.f("alice"). lookup does not:
error[E0271]: type mismatch resolving `<str as ToOwned>::Owned == &str`
note: required by a bound in `History::lookup`
Its body is self.keys.get(key), and KeyTable::get already has the right bound. The ToOwned<Owned = K> came from intern, which genuinely needs it to create the entry. Dropping it from lookupbreaks nothing — it strictly widens what compiles.
Suggested extras
HistoryBuilder::gamma(f64) shorthand — GAMMA is a public constant and drift is the most-tuned parameter after sigma, but setting it requires discovering ConstantDrift. Must pin D = ConstantDrift, so it belongs on that impl only.
Not boxing the observer: it makes the type name worse, and the reorder already gets you to History<String> at zero runtime cost. The existing Observer for Arc<O>/Box<O>/&O impls already give runtime choice and work.
Not an owned Joint: the borrow is correct design doing invalidation work a cache would need by hand, and the factorisation is n² floats.
Breaks: every explicit History<..>/Joint<..> spelling; builder_with_key; call sites that relied on &&K inference lose an &.
Found by an API audit, 2026-09-09. Both prototypes compile.
Two changes would take the realistic "fit, query, predict, store" program from unusable-without-reading-source to obvious. Both were prototyped and compile.
## The headline
```rust
let rows: Vec<(String, String, bool)> = load_csv(); // names arrive owned
let mut history = History::default();
history.record_winner(w, l, i as i64)?;
```
```
error[E0277]: the trait bound `&str: Borrow<String>` is not satisfied
error[E0271]: type mismatch resolving `<String as ToOwned>::Owned == &str`
```
Nothing there names the cause: `History::default()` silently pinned `K = &'static str`. The fix, after `History::builder_with_key::<String>()` also fails (E0107 — `K` is on the `impl`, not the `fn`, so it cannot be turbofished), is:
```rust
let mut history = History::<i64, _, _, String>::builder_with_key().build();
```
And at the *default* key type the natural call is also wrong — `h.record_winner("alice", "bob", 1)` fails; you must write `&"alice"`.
## 1. `K` is the only parameter people change, and it is last
```rust
struct Ladder { history: History<i64, ConstantDrift, NullObserver, String> }
struct Analysis<'h> { joint: Joint<'h, i64, ConstantDrift, NullObserver, &'static str> }
```
Four parameters named, three of them defaults, to change the one that matters. Reordering `K` first was prototyped and compiles — bounds may reference later parameters, so `D: Drift<T> = ConstantDrift` is legal in third position:
```rust
pub struct History<
K: Eq + Hash + Clone = &'static str,
T: Time = i64,
D: Drift<T> = ConstantDrift,
O: Observer<T> = NullObserver,
> { .. }
struct Ladder { history: History<String> } // the whole spelling
```
Pair it with a turbofishable setter, mirroring the existing type-changing `drift`/`observer`:
```rust
impl<K, T, D, O> HistoryBuilder<K, T, D, O> {
pub fn key<K2: Eq + Hash + Clone>(self) -> HistoryBuilder<K2, T, D, O>;
}
let h = History::builder().key::<String>().p_draw(0.05).build();
```
That retires `builder_with_key` (see also #68, which needs the same treatment for `T`).
Also: `Joint` carries `O` but never uses it — nothing in its body touches the observer; it rides along inside `&'h History`. With the reorder, `Joint<'h, K>` is the whole spelling.
## 2. `&[&[&K]]` is the worst shape in the API, and it is a *third* vocabulary
The crate has four ways to say "some teams of competitors": typed `Event`/`Team`/`Member`, `&[&[&K]]` (all seven `predict_*`/`expected_*`), `&[&[Rating]]` (`Game`, free `expected_information_gain`), and `&[&[Gaussian]]` (`quality`).
At the default key type the guessed spelling fails:
```rust
h.predict_win_probabilities(&[&["alice", "bob"]])
// error[E0308]: expected `&&str`, found `&str`
```
At `K = String` — the realistic case — string literals are **impossible** and you must build three levels of temporaries that all outlive the call. This is the real code required to ask "who wins":
```rust
fn head_to_head(&self, a: &str, b: &str) -> f64 {
let ta = vec![a.to_string()];
let tb = vec![b.to_string()];
let ra: Vec<&String> = ta.iter().collect();
let rb: Vec<&String> = tb.iter().collect();
let teams: Vec<&[&String]> = vec![&ra, &rb];
self.history.predict_win_probabilities(&teams).unwrap()[0]
}
```
Six lines and four allocations. The same roster said the ingestion way is `.team(names)` — one expression.
**This costs nothing to fix.** `member_skills` and `resolve_terms` do exactly two things with a key: `self.keys.get(*key)` and `format!("{key:?}")`. Neither needs `K` itself:
```rust
pub fn predict_win_probabilities<Q>(&self, teams: &[&[&Q]]) -> Result<Vec<f64>, InferenceError>
where K: Borrow<Q>, Q: Hash + Eq + Debug + ?Sized;
pub fn posterior_of<Q>(&self, terms: &[(&Q, f64)]) -> Result<Gaussian, InferenceError>
where K: Borrow<Q>, Q: Hash + Eq + Debug + ?Sized;
```
Prototyped and run. It gives the **same natural spelling for both key types**:
```rust
h.predict_win_probabilities(&[&["alice"], &["bob"]]) // K = String ✔
h.predict_win_probabilities(&[&["alice"], &["bob"]]) // K = &'static str ✔
h.posterior_of(&[("alice", 1.0), ("bob", -1.0)]) // no `&` on keys
```
A `Vec<String>` roster then needs one `.map(String::as_str)` instead of three nested collects. Bonus: `K: Debug` becomes `Q: Debug`, so the key type no longer has to be `Debug` to run a prediction.
## 3. `lookup`'s bound is copy-paste and strictly too narrow
`current_skill`, `rating`, `learning_curve`, `filtered_learning_curve` and `log_evidence_for` all accept `h.f("alice")`. `lookup` does not:
```
error[E0271]: type mismatch resolving `<str as ToOwned>::Owned == &str`
note: required by a bound in `History::lookup`
```
Its body is `self.keys.get(key)`, and `KeyTable::get` already has the right bound. The `ToOwned<Owned = K>` came from `intern`, which genuinely needs it to create the entry. Dropping it from `lookup` **breaks nothing** — it strictly widens what compiles.
## Suggested extras
- `HistoryBuilder::gamma(f64)` shorthand — `GAMMA` is a public constant and drift is the most-tuned parameter after `sigma`, but setting it requires discovering `ConstantDrift`. Must pin `D = ConstantDrift`, so it belongs on that impl only.
- **Not** boxing the observer: it makes the type name worse, and the reorder already gets you to `History<String>` at zero runtime cost. The existing `Observer for Arc<O>`/`Box<O>`/`&O` impls already give runtime choice and work.
- **Not** an owned `Joint`: the borrow is correct design doing invalidation work a cache would need by hand, and the factorisation is n² floats.
**Breaks:** every explicit `History<..>`/`Joint<..>` spelling; `builder_with_key`; call sites that relied on `&&K` inference lose an `&`.
Found by an API audit, 2026-09-09. Both prototypes compile.
Items 2 and 3 are done, plus the gamma shorthand, in 92ae5fc (merged as 4c2e98c). Item 1 — the type-parameter reorder — is still open and is the only piece left.
2 — &[&[&Q]], exactly as prototyped
All seven predict_* / expected_* methods, posterior_of, posterior_of_at and the three Joint mirrors. Verified at both key types:
h.predict_win_probabilities(&[&["alice"],&["bob"]])// K = String ✔
h.predict_win_probabilities(&[&["alice"],&["bob"]])// K = &'static str ✔
h.posterior_of(&[("alice",1.0),("bob",-1.0)])// no & on the keys ✔
h.lookup("alice")// ✔
K: Debug became Q: Debug, so a key type no longer has to be Debug to run a prediction.
Two things worth knowing that the prototype did not surface:
The old &[&[&"a"]] spelling still compiles at the default key type: Q infers to &str, and &[&[&&str]] is what both shapes resolve to. So at K = &'static str this is not a break at all — only the K = String case changes, where nothing compiled before.
predict_outcome(&[]) no longer infers Q — nothing in an empty slice names it, so it needs let none: &[&[&str]] = &[];. That is the entire cost, and it bites only on the degenerate call. One test needed the annotation.
3 — lookup
Bound dropped. KeyTable::get already had the right one, and the ToOwned<Owned = K> came from intern, which needs it to create the entry. lookup never creates.
The gamma shorthand
HistoryBuilder::gamma(f64), on the ConstantDrift impl only. It rejects a negative value for the same reason as sigma and beta — it enters squared, so -0.5 would silently behave as 0.5. tests/key_ergonomics.rs pins it against the long spelling and carries a control (a different gamma must give a different answer) so it cannot pass by being a no-op.
Item 1 — left for you
The reorder to History<K, T, D, O> is right and your prototype settles that it compiles. I have not done it because it rewrites every explicit History<..> and Joint<..> spelling in the crate and downstream, and it is the one change here where the shape is a judgement rather than a fix — in particular whether Joint should drop O at the same time (it carries the parameter and never touches the observer, so Joint<'h, K> would be the whole spelling).
Say the word and it is one mechanical pass. It pairs naturally with a turbofishable HistoryBuilder::key<K2>() replacing builder_with_key; note key_type::<K2>() and time_type::<T2>() already exist and do exactly that, so that part may already be covered.
Items 2 and 3 are done, plus the `gamma` shorthand, in 92ae5fc (merged as 4c2e98c). Item 1 — the type-parameter reorder — is still open and is the only piece left.
## 2 — `&[&[&Q]]`, exactly as prototyped
All seven `predict_*` / `expected_*` methods, `posterior_of`, `posterior_of_at` and the three `Joint` mirrors. Verified at both key types:
```rust
h.predict_win_probabilities(&[&["alice"], &["bob"]]) // K = String ✔
h.predict_win_probabilities(&[&["alice"], &["bob"]]) // K = &'static str ✔
h.posterior_of(&[("alice", 1.0), ("bob", -1.0)]) // no & on the keys ✔
h.lookup("alice") // ✔
```
`K: Debug` became `Q: Debug`, so a key type no longer has to be `Debug` to run a prediction.
**Two things worth knowing that the prototype did not surface:**
- The old `&[&[&"a"]]` spelling **still compiles** at the default key type: `Q` infers to `&str`, and `&[&[&&str]]` is what both shapes resolve to. So at `K = &'static str` this is not a break at all — only the `K = String` case changes, where nothing compiled before.
- `predict_outcome(&[])` no longer infers `Q` — nothing in an empty slice names it, so it needs `let none: &[&[&str]] = &[];`. That is the entire cost, and it bites only on the degenerate call. One test needed the annotation.
## 3 — `lookup`
Bound dropped. `KeyTable::get` already had the right one, and the `ToOwned<Owned = K>` came from `intern`, which needs it to create the entry. `lookup` never creates.
## The `gamma` shorthand
`HistoryBuilder::gamma(f64)`, on the `ConstantDrift` impl only. It rejects a negative value for the same reason as `sigma` and `beta` — it enters squared, so `-0.5` would silently behave as `0.5`. `tests/key_ergonomics.rs` pins it against the long spelling and carries a control (a different gamma must give a different answer) so it cannot pass by being a no-op.
## Item 1 — left for you
The reorder to `History<K, T, D, O>` is right and your prototype settles that it compiles. I have not done it because it rewrites every explicit `History<..>` and `Joint<..>` spelling in the crate and downstream, and it is the one change here where the *shape* is a judgement rather than a fix — in particular whether `Joint` should drop `O` at the same time (it carries the parameter and never touches the observer, so `Joint<'h, K>` would be the whole spelling).
Say the word and it is one mechanical pass. It pairs naturally with a turbofishable `HistoryBuilder::key<K2>()` replacing `builder_with_key`; note `key_type::<K2>()` and `time_type::<T2>()` already exist and do exactly that, so that part may already be covered.
Item 1 is done, so this closes. b553c63 (merged as 1ad789c).
History<K, T, D, O> and HistoryBuilder<K, T, D, O>, all four defaulted. Your prototype was right that bounds may reference later parameters, so D: Drift<T> = ConstantDrift sits fine in third position. The headline compiles:
72 call sites swapped, and the reorder made most of them shorter rather than merely reordered — 18 now read History<String>, and the &'static str ones collapse to bare History. The two turbofished builders went from HistoryBuilder::<Untimed, _, _, String>::new() to HistoryBuilder::<String, Untimed>::new().
One deviation.Joint keeps O, defaulted rather than removed. You are right that it never touches the observer — I checked what it actually reads from the history (beta, competitors, score_sigma, sigma, time_slices, resolve_terms) and none of it is O. But it borrows the whole &'h History<K, T, D, O> and calls History::resolve_terms, so dropping the parameter means introducing a view struct or moving that method off History. The default buys the entire user-visible benefit — Joint<'h> and Joint<'h, String> both spell out — so the refactor would be paying real structural churn for a parameter nobody types. Happy to do it if you want the parameter genuinely gone rather than merely invisible.
builder_with_key was already retired in favour of key_type::<K2>() / time_type::<T2>(), which are the turbofishable setters this item asked for.
Item 1 is done, so this closes. b553c63 (merged as 1ad789c).
`History<K, T, D, O>` and `HistoryBuilder<K, T, D, O>`, all four defaulted. Your prototype was right that bounds may reference later parameters, so `D: Drift<T> = ConstantDrift` sits fine in third position. The headline compiles:
```rust
struct Ladder { history: History<String> }
struct Analysis<'h> { joint: Joint<'h> }
```
72 call sites swapped, and the reorder made most of them **shorter** rather than merely reordered — 18 now read `History<String>`, and the `&'static str` ones collapse to bare `History`. The two turbofished builders went from `HistoryBuilder::<Untimed, _, _, String>::new()` to `HistoryBuilder::<String, Untimed>::new()`.
**One deviation.** `Joint` keeps `O`, defaulted rather than removed. You are right that it never touches the observer — I checked what it actually reads from the history (`beta`, `competitors`, `score_sigma`, `sigma`, `time_slices`, `resolve_terms`) and none of it is `O`. But it borrows the whole `&'h History<K, T, D, O>` and calls `History::resolve_terms`, so dropping the parameter means introducing a view struct or moving that method off `History`. The default buys the entire user-visible benefit — `Joint<'h>` and `Joint<'h, String>` both spell out — so the refactor would be paying real structural churn for a parameter nobody types. Happy to do it if you want the parameter genuinely gone rather than merely invisible.
`builder_with_key` was already retired in favour of `key_type::<K2>()` / `time_type::<T2>()`, which are the turbofishable setters this item asked for.
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
Two changes would take the realistic "fit, query, predict, store" program from unusable-without-reading-source to obvious. Both were prototyped and compile.
The headline
Nothing there names the cause:
History::default()silently pinnedK = &'static str. The fix, afterHistory::builder_with_key::<String>()also fails (E0107 —Kis on theimpl, not thefn, so it cannot be turbofished), is:And at the default key type the natural call is also wrong —
h.record_winner("alice", "bob", 1)fails; you must write&"alice".1.
Kis the only parameter people change, and it is lastFour parameters named, three of them defaults, to change the one that matters. Reordering
Kfirst was prototyped and compiles — bounds may reference later parameters, soD: Drift<T> = ConstantDriftis legal in third position:Pair it with a turbofishable setter, mirroring the existing type-changing
drift/observer:That retires
builder_with_key(see also #68, which needs the same treatment forT).Also:
JointcarriesObut never uses it — nothing in its body touches the observer; it rides along inside&'h History. With the reorder,Joint<'h, K>is the whole spelling.2.
&[&[&K]]is the worst shape in the API, and it is a third vocabularyThe crate has four ways to say "some teams of competitors": typed
Event/Team/Member,&[&[&K]](all sevenpredict_*/expected_*),&[&[Rating]](Game, freeexpected_information_gain), and&[&[Gaussian]](quality).At the default key type the guessed spelling fails:
At
K = String— the realistic case — string literals are impossible and you must build three levels of temporaries that all outlive the call. This is the real code required to ask "who wins":Six lines and four allocations. The same roster said the ingestion way is
.team(names)— one expression.This costs nothing to fix.
member_skillsandresolve_termsdo exactly two things with a key:self.keys.get(*key)andformat!("{key:?}"). Neither needsKitself:Prototyped and run. It gives the same natural spelling for both key types:
A
Vec<String>roster then needs one.map(String::as_str)instead of three nested collects. Bonus:K: DebugbecomesQ: Debug, so the key type no longer has to beDebugto run a prediction.3.
lookup's bound is copy-paste and strictly too narrowcurrent_skill,rating,learning_curve,filtered_learning_curveandlog_evidence_forall accepth.f("alice").lookupdoes not:Its body is
self.keys.get(key), andKeyTable::getalready has the right bound. TheToOwned<Owned = K>came fromintern, which genuinely needs it to create the entry. Dropping it fromlookupbreaks nothing — it strictly widens what compiles.Suggested extras
HistoryBuilder::gamma(f64)shorthand —GAMMAis a public constant and drift is the most-tuned parameter aftersigma, but setting it requires discoveringConstantDrift. Must pinD = ConstantDrift, so it belongs on that impl only.History<String>at zero runtime cost. The existingObserver for Arc<O>/Box<O>/&Oimpls already give runtime choice and work.Joint: the borrow is correct design doing invalidation work a cache would need by hand, and the factorisation is n² floats.Breaks: every explicit
History<..>/Joint<..>spelling;builder_with_key; call sites that relied on&&Kinference lose an&.Found by an API audit, 2026-09-09. Both prototypes compile.
Items 2 and 3 are done, plus the
gammashorthand, in92ae5fc(merged as4c2e98c). Item 1 — the type-parameter reorder — is still open and is the only piece left.2 —
&[&[&Q]], exactly as prototypedAll seven
predict_*/expected_*methods,posterior_of,posterior_of_atand the threeJointmirrors. Verified at both key types:K: DebugbecameQ: Debug, so a key type no longer has to beDebugto run a prediction.Two things worth knowing that the prototype did not surface:
&[&[&"a"]]spelling still compiles at the default key type:Qinfers to&str, and&[&[&&str]]is what both shapes resolve to. So atK = &'static strthis is not a break at all — only theK = Stringcase changes, where nothing compiled before.predict_outcome(&[])no longer infersQ— nothing in an empty slice names it, so it needslet none: &[&[&str]] = &[];. That is the entire cost, and it bites only on the degenerate call. One test needed the annotation.3 —
lookupBound dropped.
KeyTable::getalready had the right one, and theToOwned<Owned = K>came fromintern, which needs it to create the entry.lookupnever creates.The
gammashorthandHistoryBuilder::gamma(f64), on theConstantDriftimpl only. It rejects a negative value for the same reason assigmaandbeta— it enters squared, so-0.5would silently behave as0.5.tests/key_ergonomics.rspins it against the long spelling and carries a control (a different gamma must give a different answer) so it cannot pass by being a no-op.Item 1 — left for you
The reorder to
History<K, T, D, O>is right and your prototype settles that it compiles. I have not done it because it rewrites every explicitHistory<..>andJoint<..>spelling in the crate and downstream, and it is the one change here where the shape is a judgement rather than a fix — in particular whetherJointshould dropOat the same time (it carries the parameter and never touches the observer, soJoint<'h, K>would be the whole spelling).Say the word and it is one mechanical pass. It pairs naturally with a turbofishable
HistoryBuilder::key<K2>()replacingbuilder_with_key; notekey_type::<K2>()andtime_type::<T2>()already exist and do exactly that, so that part may already be covered.Item 1 is done, so this closes.
b553c63(merged as1ad789c).History<K, T, D, O>andHistoryBuilder<K, T, D, O>, all four defaulted. Your prototype was right that bounds may reference later parameters, soD: Drift<T> = ConstantDriftsits fine in third position. The headline compiles:72 call sites swapped, and the reorder made most of them shorter rather than merely reordered — 18 now read
History<String>, and the&'static strones collapse to bareHistory. The two turbofished builders went fromHistoryBuilder::<Untimed, _, _, String>::new()toHistoryBuilder::<String, Untimed>::new().One deviation.
JointkeepsO, defaulted rather than removed. You are right that it never touches the observer — I checked what it actually reads from the history (beta,competitors,score_sigma,sigma,time_slices,resolve_terms) and none of it isO. But it borrows the whole&'h History<K, T, D, O>and callsHistory::resolve_terms, so dropping the parameter means introducing a view struct or moving that method offHistory. The default buys the entire user-visible benefit —Joint<'h>andJoint<'h, String>both spell out — so the refactor would be paying real structural churn for a parameter nobody types. Happy to do it if you want the parameter genuinely gone rather than merely invisible.builder_with_keywas already retired in favour ofkey_type::<K2>()/time_type::<T2>(), which are the turbofishable setters this item asked for.