std/hash¶
Non-cryptographic hashing building blocks: free functions that hash the bytes
of a String or mix an Int. They are fast, well-known mixing functions meant
for hash tables, checksums, and folding several values into one composite hash.
import std/hash { fnv1a }
fun main() {
print(fnv1a("abc")) // a 64-bit FNV-1a hash, may be negative
}
Not cryptographic¶
These functions are not cryptographic. Do not use them for passwords, signatures, integrity against an adversary, or any security purpose. They make no collision-resistance or preimage guarantees. Cryptographic hashes (SHA-2, BLAKE3, and similar) are out of scope for the standard library and belong in a package.
What they are good for: keys in a hash table, quick change detection, and combining the hashes of a struct's fields.
Importing¶
Every entry is a free function, so bring in the ones you need with a selective import:
A note on integers¶
Raven Int is i64 and arithmetic wraps in two's complement. That wrapping is
the intended behavior here: the FNV multiply, the djb2 multiply, and the
integer mixes all rely on it. Returned hashes use the full i64 range and may be
negative.
fnv1a uses the standard FNV-64 offset basis (14695981039346656037, written
as its i64 two's-complement value) and prime (1099511628211), so it
reproduces the canonical FNV-1a 64-bit vectors (for example fnv1a("abc") is
0xe71fa2190541574b).
>> is an arithmetic (sign-propagating) shift. The integer mixes stay
deterministic under it; the values differ from an unsigned-shift reference, but
that does not matter for a non-cryptographic hash.
Surface¶
| Function | Result | Notes |
|---|---|---|
fnv1a(s: String) |
Int |
FNV-1a over the bytes of s, 64-bit variant |
djb2(s: String) |
Int |
classic djb2 string hash (hash * 33 + byte, seed 5381) |
hash_int(n: Int) |
Int |
splitmix64-style bit-mix so sequential ints scatter |
combine(seed: Int, value: Int) |
Int |
fold two hashes into one (boost hash_combine) |
crc32(s: String) |
Int |
CRC-32 IEEE checksum, in [0, 2^32) |
checksum(s: String) |
Int |
additive byte checksum; cheap and weak, change detection only |
Hashing strings¶
fnv1a(s: String) -> Int¶
FNV-1a over the bytes of s, the 64-bit variant. A good general-purpose string
hash with strong avalanche behavior for short keys. Prefer this when you want
one string hash and are unsure which to pick.
import std/hash { fnv1a }
fun main() {
print(fnv1a("abc")) // 0xe71fa2190541574b as a signed i64
print(fnv1a("")) // the offset basis, for the empty string
}
djb2(s: String) -> Int¶
The classic djb2 hash: start at 5381, then hash = hash * 33 + byte for each
byte. Simple and fast; included as a well-known alternative to fnv1a.
crc32(s: String) -> Int¶
The CRC-32 IEEE checksum of the bytes of s. Unlike the i64 hashes above,
the result is always in [0, 2^32) and never negative. A checksum for change
detection, not for security.
checksum(s: String) -> Int¶
A plain additive checksum: the sum of the byte values in s. Cheaper and
weaker than the hashes above, so use it only for quick change detection (did
this string change?), not for distributing keys across buckets.
Mixing integers¶
hash_int(n: Int) -> Int¶
A splitmix64-style bit-mix. Sequential integers (0, 1, 2, ...) hash to
scattered, well-spread values, which is what you want before using them as
table indices.
import std/hash { hash_int }
fun main() {
print(hash_int(0))
print(hash_int(1)) // far from hash_int(0)
}
combine(seed: Int, value: Int) -> Int¶
Fold value into seed and return the mixed result (the boost hash_combine
recipe, using the 32-bit golden ratio constant). Chain it to hash a sequence of
values into a single hash:
import std/hash { hash_int, combine }
fun main() {
let h = hash_int(1)
h = combine(h, hash_int(2))
h = combine(h, hash_int(3))
print(h) // one hash that depends on all three values, in order
}
Order matters: combine is not commutative, so [1, 2] and [2, 1] produce
different hashes.
Relationship to the Hash trait¶
The prelude defines trait Hash { fun hash(self) -> Int } with built-in impls
for Int, Bool, and String, which is what std/collections
uses to key its hash-based containers. These functions are the building blocks
a user's own Hash impl can call: hash each field, then fold the results with
combine.
import std/hash { hash_int, combine }
struct Point { x: Int, y: Int }
impl Hash for Point {
fun hash(self) -> Int {
let h = hash_int(self.x)
combine(h, hash_int(self.y))
}
}
Worked example: hashing a record¶
Hash a small struct by mixing a string field and an integer field into one value, suitable for use as a key.
import std/hash { fnv1a, hash_int, combine }
struct User { name: String, id: Int }
fun hash_user(u: User) -> Int {
let h = fnv1a(u.name)
return combine(h, hash_int(u.id))
}
fun main() {
let a = User { name: "ada", id: 1 }
let b = User { name: "ada", id: 2 }
print(hash_user(a) == hash_user(a)) // true, hashing is deterministic
print(hash_user(a) == hash_user(b)) // false, the id differs
}
See also¶
- std/collections for the hash-based containers that build on
the
Hashtrait. - std/string for the string methods you may call before hashing.