Skip to content

Allow Counter::new() without naming the hasher - #59

Open
tb158 wants to merge 1 commit into
coriolinus:masterfrom
tb158:feat/inferable-constructors
Open

tb158 wants to merge 1 commit into
coriolinus:masterfrom
tb158:feat/inferable-constructors

Conversation

@tb158

@tb158 tb158 commented Aug 9, 2026

Copy link
Copy Markdown

Problem

Counter::new() cannot be called without a turbofish or a type annotation:

let counter = Counter::new();
// error[E0283]: type annotations needed
//               cannot satisfy `_: Default`

Default type parameters are only applied where a type is written out; they are never used during inference. new lives in the single impl<T, N, S> Counter<T, N, S> where S: Default block, so S is left ambiguous and every caller has to name a hasher they usually do not care about:

let counter = Counter::<char>::new();
let counter: Counter<char> = Counter::new();

HashMap::new() does not have this problem because std keeps it in a separate impl<K, V> HashMap<K, V, RandomState> block.

Approach

The same shape std uses: constructors which do not take a hasher pin S = RandomState; constructors which do take one are named for it.

before after
default hasher Counter::new() (needs annotation) Counter::new()
Counter::with_capacity(n) (needs annotation) Counter::with_capacity(n)
custom hasher Counter::<_, _, S>::new() Counter::with_hasher(s)
Counter::<_, _, S>::with_capacity(n) Counter::with_capacity_and_hasher(n, s)

with_hasher takes the hasher by value instead of requiring S: Default, which also makes seeded hashers usable.

Adding a RandomState-pinned new alongside the generic one is not an option: the two inherent impls overlap, giving error[E0592]: duplicate definitions with name 'new' plus an error[E0034] at every internal call site. Moving the method is the only way to get the short form.

What is deliberately unchanged

Default stays generic over S, matching impl<K, V, S: Default> Default for HashMap<K, V, S>. Counter::default() therefore still needs S pinned by context, exactly as HashMap::default() does. Restricting that impl would fix the bare call too, but it would break #[derive(Default)] on any type holding a custom-hasher Counter, and Counter::new() now covers the case which motivated this change.

Breaking change

new and with_capacity no longer accept a custom hasher. The failure is a clear error[E0308]:

expected `Counter<char, usize, MyHasher>`, found `Counter<_, _, RandomState>`

Migration:

- let counter: Counter<char, usize, MyHasher> = Counter::new();
+ let counter: Counter<char, usize, MyHasher> = Counter::with_hasher(MyHasher::default());
 
- let counter: Counter<char, usize, MyHasher> = Counter::with_capacity(n);
+ let counter: Counter<char, usize, MyHasher> = Counter::with_capacity_and_hasher(n, MyHasher::default());

Custom hashers have only been supported since 0.7.0 (#51), so the affected surface is small. The commit is marked feat! so git cliff picks it up as breaking.

Also included

  • Doc comments for the four constructors, and "Start from an empty counter" / "Use a custom hasher" sections in the crate docs — custom hasher support was previously undocumented outside the type signature.
  • Unit tests for annotation-free new, for with_hasher and with_capacity_and_hasher, and an integration test which constructs with a non-default hasher.
    cargo test --all-features (21 unit + 19 integration + 51 doctests), cargo clippy --all-features --all-targets, and cargo doc are clean.

`Counter::new()` could not be called without a turbofish or a type
annotation. Default type parameters are only applied where a type is
written out; they are never used during inference, so the single
`impl<T, N, S> Counter<T, N, S> where S: Default` block left `S`
ambiguous:

    let counter = Counter::new();  // error[E0283]

Adding a `RandomState`-pinned `new` alongside the generic one is not
possible: the two inherent impls overlap, which is `error[E0592]`.
Instead, follow the shape `std` uses for `HashMap`: pin the hasher in
the constructors which do not take one, and name the ones which do.

    let mut counter = Counter::new();               // RandomState
    let mut counter = Counter::with_hasher(hasher); // any BuildHasher

`Default` is deliberately left generic over `S`, matching
`impl<K, V, S: Default> Default for HashMap<K, V, S>`. Pinning it would
break `#[derive(Default)]` on types holding a custom-hasher `Counter`,
and `Counter::new()` now covers the case which needed an annotation.

BREAKING CHANGE: `new` and `with_capacity` no longer accept a custom
hasher. Replace `Counter::<T, N, S>::new()` with
`Counter::with_hasher(S::default())` and
`Counter::<T, N, S>::with_capacity(n)` with
`Counter::with_capacity_and_hasher(n, S::default())`.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@qodo-code-review

Copy link
Copy Markdown

ⓘ Qodo reviews are paused because the subscription is no longer active. Ask your workspace admin to reactivate the subscription to resume reviews. Manage billing

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant