The Error
You tried to use a floating-point number as a key in a HashMap and hit this at compile time:
error[E0277]: the trait bound `f64: Hash` is not satisfied
--> src/main.rs:5:36
|
5 | let mut map: HashMap<f64, i32> = HashMap::new();
| ^^^^^^^^^^^^^^^ the trait `Hash` is not implemented for `f64`
|
= help: the following other types implement trait `Hash`: ...
note: required by a bound in `HashMap`
Why f64 Can't Be a HashMap Key
Rust's HashMap requires keys to implement two traits: Hash and Eq. Floating-point types (f32 and f64) implement neither โ and for good reason.
The problem is NaN. IEEE 754 specifies that NaN != NaN, which directly violates the contract that Eq requires (every value must equal itself). And since you can't have a sound Eq implementation, you also can't have a sound Hash implementation โ a key that isn't equal to itself would break the entire hash table invariant.
So Rust simply doesn't implement these traits for floats. This isn't a limitation you can work around with a feature flag; it's a deliberate design choice to prevent silent data corruption.
Fix 1: Use the ordered_float Crate (Recommended)
The cleanest solution for most cases is the ordered_float crate. It provides two wrapper types:
OrderedFloat<f64>โ allows any float includingNaN(treats all NaNs as equal)NotNan<f64>โ panics or returns an error if you try to storeNaN
Add it to your Cargo.toml:
[dependencies]
ordered-float = "4"
Then use it as your key type:
use ordered_float::NotNan;
use std::collections::HashMap;
fn main() {
let mut map: HashMap<NotNan<f64>, &str> = HashMap::new();
let key = NotNan::new(3.14).expect("key is NaN");
map.insert(key, "pi");
let lookup = NotNan::new(3.14).unwrap();
println!("{:?}", map.get(&lookup)); // Some("pi")
}
Use NotNan when NaN is genuinely an invalid state in your domain โ it's the safer option and makes the invalid state unrepresentable. Use OrderedFloat if you legitimately need to store NaN keys.
Fix 2: Convert to Bits and Use u64 as Key
If you control the code and you're sure your values are never NaN or -0.0, you can convert f64 to its raw bit representation (u64) and use that as the key:
use std::collections::HashMap;
fn f64_key(v: f64) -> u64 {
assert!(!v.is_nan(), "NaN cannot be used as a map key");
v.to_bits()
}
fn main() {
let mut map: HashMap<u64, &str> = HashMap::new();
map.insert(f64_key(1.5), "one and a half");
map.insert(f64_key(-0.75), "negative three quarters");
println!("{:?}", map.get(&f64_key(1.5))); // Some("one and a half")
}
One gotcha: 0.0 and -0.0 are equal by IEEE 754 comparison but have different bit representations. If that distinction matters in your map, you need to normalize them first:
fn f64_key(v: f64) -> u64 {
assert!(!v.is_nan());
// Treat +0.0 and -0.0 as the same key
if v == 0.0 { 0u64.to_bits() } else { v.to_bits() }
}
Fix 3: Custom Wrapper with Manual Hash + Eq
If you want full control and don't want an external dependency, you can write a thin wrapper:
use std::collections::HashMap;
use std::hash::{Hash, Hasher};
#[derive(Clone, Copy, PartialEq)]
struct FloatKey(f64);
impl Eq for FloatKey {}
impl Hash for FloatKey {
fn hash<H: Hasher>(&self, state: &mut H) {
// Same normalization as fix 2
let bits = if self.0.is_nan() {
f64::NAN.to_bits()
} else if self.0 == 0.0 {
0u64
} else {
self.0.to_bits()
};
bits.hash(state);
}
}
fn main() {
let mut map: HashMap<FloatKey, &str> = HashMap::new();
map.insert(FloatKey(2.718), "euler");
println!("{:?}", map.get(&FloatKey(2.718))); // Some("euler")
}
This works, but it's boilerplate you're now responsible for maintaining. The ordered_float crate does this (and does it more carefully) for you.
Fix 4: Rethink Your Key Type
This is the fix that actually applies most often. If you're building a map keyed by floating-point numbers, ask yourself: is a float really the right key here?
- Frequency bins โ use integer bin index instead of the center frequency
- Prices โ use integer cents/satoshis instead of dollars
- Coordinates โ round to a fixed precision and use a string or integer key
- Lookup by sensor reading โ bucket the values into ranges first
Floating-point equality is inherently fuzzy, so a map keyed by exact float values is often a sign that the design could be reconsidered.
Verify the Fix
After applying any of the above fixes, run:
cargo build
The E0277 error disappears. Then run your tests:
cargo test
Add a quick sanity check if you haven't already:
#[cfg(test)]
mod tests {
use super::*;
use ordered_float::NotNan;
use std::collections::HashMap;
#[test]
fn float_key_roundtrip() {
let mut map: HashMap<NotNan<f64>, i32> = HashMap::new();
let k = NotNan::new(1.23).unwrap();
map.insert(k, 42);
assert_eq!(map[&NotNan::new(1.23).unwrap()], 42);
}
}
Tips
If you're debugging why two float values that look equal aren't matching as map keys, it helps to inspect their raw bit representation. You can use println!("{:064b}", my_f64.to_bits()) in Rust, or for quick checks outside your code, a hash/bit inspector like the one at ToolCraft's Hash Generator โ useful for verifying that two byte sequences are truly identical when you're not sure if a float value is being serialized consistently.
Also worth knowing: this same issue applies to f32. The ordered_float crate supports both OrderedFloat<f32> and NotNan<f32>.
Finally, if you're coming from Python or JavaScript where this just works, keep in mind those languages are making an implicit and potentially buggy choice for you. Rust makes you be explicit, which saves you from subtle key-collision bugs in production.

