#Hash
Status: standard library specification draft.
This chapter defines the part of std.hash that lib/std writes in
ordinary hd over the language tier:
- what a derived
Hashhashes, through the trait's template; - how tuples hash, through the trait's tuple template;
- how lists and results hash;
- the bytes each standard implementation writes;
- the standard hasher
DefaultHasher, andhash_of.
The language tier keeps Hash and Hasher, which map keys require
(Hashing), and the @derive checks.
#Derived Hashing
@derive(Hash) hashes a value member by member:
@derive(Eq, Hash)
enum Status:
Ready(code: i32)
Failed(code: i32, reason: string)
fn lookup_status(items: Map[Status, i32], status: Status) -> i32?:
items.get(status)- std-hash.derive.hash.template
std.hashdeclares the template ofHash, which@derive(Hash)instantiates. - std-hash.derive.hash.support
@derive(Hash)supports structs and enums. - std-hash.derive.hash.dataIt hashes every declared struct field in declaration order, including embedded fields.
- std-hash.derive.hash.enumFor an enum, it hashes the variant, then shared enum data in declaration order, then that variant's payload fields in declaration order.
- std-hash.derive.hash.fieldsEvery hashed field must implement
Hash; no field is implicitly excluded. - std-hash.derive.hash.cyclesLike derived equality, derived hashing does not detect cycles, so hashing a cyclic graph may exhaust the execution stack.
- std-hash.derive.hash.referencesA reference field hashes as the value it refers to, by Reference Hashing.
#Tuple Hashing
fn index(counts: Map[(i32, string), i32]) -> i32:
counts[(1, "a")]- std-hash.tuple.template
std.hashdeclares a tuple template forHash. A tuple implementsHashwhen every element does, at every size. - std-hash.tuple.rest-listA tuple's rest element hashes as its
List[T]. So a rest tuple implementsHashwhen its fixed elements do andT < Hash, and it may be a map key.
#List Hashing
fn count(counts: Map[List[i32], i32], key: List[i32]) -> i32:
match counts.get(key):
.Some(n) => n
.None => 0- std-hash.list.impl
std.hashimplementsHashforList[T]whenT < Hash. - std-hash.list.not-elementA list whose item type does not implement
Hash, such asList[f64], does not implementHash. Using it whereHashis required is an error. Error:unsatisfied-trait-bound.
fn lookup(counts: Map[List[f64], i32]) -> i32: # error: unsatisfied-trait-bound
0#Result Hashing
fn failures(counts: Map[Result[i32, string], i32], key: Result[i32, string]) -> i32:
match counts.get(key):
.Some(n) => n
.None => 0- std-hash.result.impl
std.hashimplementsHashforResult[T, E]whenT < HashandE < Hash, so aResultof hashable types may be a map key.
#Bytes Written
Each standard Hash implementation writes a fixed sequence of bytes to
its Hasher:
use std.hash.hash_of
fn digest() -> u64:
let key: (u8, string) = (7, "a")
hash_of(key) # 12920292002639229810, from the bytes 7, 1, 0, 0, 0, 0, 0, 0, 0, 97| Rule | Value | Bytes written, in order |
|---|---|---|
| std-hash.bytes.integerInteger | an i8 to i64, a u8 to u64, or a usize |
its two's-complement bytes at its own width, least significant first |
std-hash.bytes.boolbool |
false or true |
one byte, 0 or 1 |
std-hash.bytes.charchar |
a Unicode scalar value | the scalar value, written as a u32 writes it |
std-hash.bytes.stringstring |
a UTF-8 text | the number of its UTF-8 bytes as a u64, then those bytes |
std-hash.bytes.listList[T] |
a list | its length as a u64, then each item's bytes in order |
| std-hash.bytes.tupleTuple | a tuple | each element's bytes in order, with no length; a rest element writes as its List |
std-hash.bytes.optionalT? |
.None or .Some(value) |
the tag byte 0 for .None; the tag byte 1, then the bytes of value, for .Some |
| std-hash.bytes.referenceReference | &T or &mut T |
the bytes of the value it refers to |
std-hash.bytes.resultResult[T, E] |
.Ok(value) or .Err(error) |
the tag byte 0, then the bytes of value, for .Ok; the tag byte 1, then the bytes of error, for .Err |
- std-hash.bytes.sequenceEach standard implementation of
Hashwrites exactly the bytes of the table above, sohash_ofof a value is the same in every implementation. - std-hash.bytes.typed-methodsA standard
Hashimplementation may write integer bytes or a string's bytes through the corresponding typedHashermethod, since it writes the same byte sequence aswrite. - std-hash.bytes.derive-enumA derived
Hashon an enum writes the variant's zero-based index as au32, before the members thatstd-hash.derive.hash.enumlists. - std-hash.bytes.derive-dataA derived
Hashon a struct writes no tag, only the bytes of its members in the order ofstd-hash.derive.hash.data.
Why. A string writes its length first, as a list does, so one rule covers every sequence, and
("ab", "c")writes other bytes than("a", "bc"). Rust instead ends a string with the byte0xff, which UTF-8 never contains. That is shorter, but it is a second scheme. A tuple writes no length because its type fixes its arity.
Note. These rules fix the bytes in order, not how an implementation splits them into
writecalls. ADefaultHashergives the same result for any split, bystd-hash.default.write.
#Default Hasher
DefaultHasher is the standard Hasher, with one fixed algorithm:
use std.hash.{DefaultHasher, hash_of}
fn digest() -> u64:
hasher := DefaultHasher::new()
hasher.write([97])
hasher.finish() # 12638187200555641996
fn bucket(key: string) -> u64:
hash_of(key)| Step | Effect on the 64-bit state |
|---|---|
| start | the FNV offset basis, 0xcbf29ce484222325 |
each byte b, in order |
XOR b into the state, then multiply by the FNV prime 0x100000001b3, modulo 2^64 |
finish |
returns the state |
- std-hash.default.decl-struct
std.hashdeclares the structDefaultHasherwith private fields, used behind*mut, and the functionhash_of. Code imports them, as inuse std.hash.{DefaultHasher, hash_of}. - std-hash.default.algorithm
DefaultHashercomputes 64-bit FNV-1a over the bytes written to it, by the table above. - std-hash.default.new-ref
DefaultHasher::new() -> *mut DefaultHasherreturns a new object, a hasher whose state is the offset basis. Copies of the reference share it, sovalue.hash(hasher)changes the state thathasher.finish()then returns. - std-hash.default.write
DefaultHasherimplementsHasher. Itswrite(bytes)processes each byte in order, so two writes give the state of one write of both byte lists joined. - std-hash.default.finish
finish(self) -> u64returns the state and does not change it. - std-hash.default.typed-writes
DefaultHasheroverrides every typedHashermethod to process the specified bytes without building a list. - std-hash.default.fixedThe algorithm has no seed and no key, so the same bytes give the same result in every run, every program, and every implementation.
- std-hash.hash-of
hash_of[T < Hash](value: T) -> u64callsvalue.hashon a newDefaultHasherand returns itsfinish(). - std-hash.map.bucket-hashThe hash by which the built-in
Mapbuckets its keys is an implementation detail. It need not behash_of, and it may differ between runs.
Why. FNV-1a is a few lines of hd that need only XOR and one wrapping multiplication. SipHash-1-3 mixes better, but its strength is a secret key. With a fixed key it gives no defense against chosen keys, so its larger code buys little here.
Note. A fixed algorithm makes hash values reproducible, so a test may assert one. It also means that keys chosen to collide stay colliding, so the built-in
Mapdoes not bucket keys by it.
Why. Keys chosen to collide make every insertion probe the whole table, and a map built from untrusted JSON takes its keys from the sender. So the reference implementation buckets every key by a hash under a random key that the host draws once per run, as Rust keys SipHash-1-3 per process and Go seeds its hash per map. A string, or the
Hashbytes of a tuple or other key, goes through HalfSipHash-1-3; an integer through the keyed multiply mix Go uses on 32-bit platforms. A REPL session's journal records the key, so a rebuild replays exactly. Iteration order does not depend on the hash (types.map.order.deterministic), so the key changes no output.
See also: Derived Tuple Implementations, Lookup And Order, Cmp.
#Reference Hashing
std.hash implements Hash for references by the values they refer to:
use std.hash.hash_of
fn same_hash(a: &string, b: &string) -> bool:
hash_of(a) == hash_of(b) # true when *a == *b- std-hash.reference.impl
std.hashimplementsHashfor&Tand for&mut TwhenT < Hash. A reference writes the bytes of the value it refers to, so a reference and its value hash alike. - std-hash.reference.derivedA derived
Hashreaches this implementation for a reference field, asstd-hash.derive.hash.referencesstates.