Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -16,3 +16,6 @@ gh-pages
# git files
.swp
/tags

# RustRover/Other jetbrains IDE files
.idea/
8 changes: 8 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,14 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](http://keepachangelog.com/)
and this project adheres to [Semantic Versioning](http://semver.org/).

## Unreleased

### Added
- Add `Hash` derive similar to `std`'s one, but considering generics correctly,
and supporting custom hash functions per field or skipping fields.
([#532](https://github.com/JelteF/derive_more/pull/532))


## 2.1.1 - 2025-12-22

### Fixed
Expand Down
12 changes: 12 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@ eq = ["derive_more-impl/eq"]
error = ["derive_more-impl/error"]
from = ["derive_more-impl/from"]
from_str = ["derive_more-impl/from_str"]
hash = ["derive_more-impl/hash"]
index = ["derive_more-impl/index"]
index_mut = ["derive_more-impl/index_mut"]
into = ["derive_more-impl/into"]
Expand Down Expand Up @@ -93,6 +94,7 @@ full = [
"error",
"from",
"from_str",
"hash",
"index",
"index_mut",
"into",
Expand Down Expand Up @@ -180,6 +182,16 @@ name = "from_str"
path = "tests/from_str.rs"
required-features = ["from_str"]

[[test]]
name = "hash"
path = "tests/hash.rs"
required-features = ["hash"]

[[test]]
name = "hash_and_eq"
path = "tests/hash_and_eq.rs"
required-features = ["hash", "eq"]

[[test]]
name = "index_mut"
path = "tests/index_mut.rs"
Expand Down
4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,8 @@ These are traits that can be used for operator overloading.
`ShrAssign` and `ShlAssign`
11. [`Eq`], [`PartialEq`]

### Other traits
1. [`Hash`]

### Static methods

Expand Down Expand Up @@ -264,6 +266,8 @@ Changing [MSRV] (minimum supported Rust version) of this crate is treated as a *
[`Eq`]: https://docs.rs/derive_more/latest/derive_more/derive.Eq.html
[`PartialEq`]: https://docs.rs/derive_more/latest/derive_more/derive.PartialEq.html

[`Hash`]: https://docs.rs/derive_more/latest/derive_more/derive.Hash.html

[`Constructor`]: https://docs.rs/derive_more/latest/derive_more/derive.Constructor.html
[`IsVariant`]: https://docs.rs/derive_more/latest/derive_more/derive.IsVariant.html
[`Unwrap`]: https://docs.rs/derive_more/latest/derive_more/derive.Unwrap.html
Expand Down
3 changes: 2 additions & 1 deletion examples/deny_missing_docs.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
#![allow(dead_code)] // for illustration purposes

use derive_more::{
Add, AddAssign, Constructor, Deref, DerefMut, Display, From, FromStr, Index,
Add, AddAssign, Constructor, Deref, DerefMut, Display, From, FromStr, Hash, Index,
IndexMut, Into, IsVariant, Mul, MulAssign, Not, TryInto,
};

Expand All @@ -18,6 +18,7 @@ fn main() {}
Display,
From,
FromStr,
Hash,
Into,
Mul,
MulAssign,
Expand Down
2 changes: 2 additions & 0 deletions impl/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ eq = ["syn/extra-traits", "syn/visit"]
error = ["syn/extra-traits"]
from = ["syn/extra-traits"]
from_str = ["syn/full", "syn/visit", "dep:convert_case"]
hash = ["syn/extra-traits", "syn/visit"]
index = []
index_mut = []
into = ["syn/extra-traits", "syn/visit-mut"]
Expand Down Expand Up @@ -88,6 +89,7 @@ full = [
"error",
"from",
"from_str",
"hash",
"index",
"index_mut",
"into",
Expand Down
266 changes: 266 additions & 0 deletions impl/doc/hash.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,266 @@
# Using `#[derive(Hash)]`

Deriving `Hash` works by hashing values according to their type structure.

## Structural hashing

Deriving `Hash` for enums/structs works in a similar way to the one in `std`,
by hashing all the available fields, but, in contrast:
1. Does not overconstrain generic parameters.
2. Allows to ignore fields, whole structs or enum variants via `#[hash(skip)]` attribute.
3. Allows to use a custom hash function for a field via `#[hash(with(function))]` attribute.

### Structs

For structs all the available fields are hashed.

```rust
# use std::marker::PhantomData;
# use derive_more::Hash;
#
trait Trait {
type Assoc;
}
impl<T: ?Sized> Trait for T {
type Assoc = u8;
}

#[derive(Debug, Hash)]
struct Foo<A, B, C: Trait + ?Sized> {
a: A,
b: PhantomData<B>,
c: C::Assoc,
}

#[derive(Debug)]
struct NoHash;
```

This generates code equivalent to:

```rust
# use std::marker::PhantomData;
# use derive_more::core::hash::{Hash, Hasher};
#
# trait Trait {
# type Assoc;
# }
# impl<T: ?Sized> Trait for T {
# type Assoc = u8;
# }
#
# struct Foo<A, B, C: Trait + ?Sized> {
# a: A,
# b: PhantomData<B>,
# c: C::Assoc,
# }
#
impl<A, B, C: Trait + ?Sized> Hash for Foo<A, B, C>
where
A: Hash,
PhantomData<B>: Hash, // `B: Hash` is generated by `std` instead
C::Assoc: Hash, // `C: Hash` is generated by `std` instead
{
fn hash<H: Hasher>(&self, state: &mut H) {
match self {
Self { a: self_0, b: self_1, c: self_2 } => {
self_0.hash(state);
self_1.hash(state);
self_2.hash(state);
}
}
}
}
```

### Enums

For enums the discriminant is hashed first, followed by the fields.

```rust
# use std::marker::PhantomData;
# use derive_more::Hash;
#
# trait Trait {
# type Assoc;
# }
# impl<T: ?Sized> Trait for T {
# type Assoc = u8;
# }
#
#[derive(Debug, Hash)]
enum Foo<A, B, C: Trait + ?Sized> {
A(A),
B { b: PhantomData<B> },
C(C::Assoc),
}
#
# #[derive(Debug)]
# struct NoHash;
```

This generates code equivalent to:

```rust
# use std::marker::PhantomData;
# use derive_more::core::hash::{Hash, Hasher};
#
# trait Trait {
# type Assoc;
# }
# impl<T: ?Sized> Trait for T {
# type Assoc = u8;
# }
#
# enum Foo<A, B, C: Trait + ?Sized> {
# A(A),
# B { b: PhantomData<B> },
# C(C::Assoc),
# }
#
impl<A, B, C: Trait + ?Sized> Hash for Foo<A, B, C>
where
A: Hash,
PhantomData<B>: Hash, // `B: Hash` is generated by `std` instead
C::Assoc: Hash, // `C: Hash` is generated by `std` instead
{
fn hash<H: Hasher>(&self, state: &mut H) {
std::mem::discriminant(self).hash(state);
match self {
Self::A(self_0) => { self_0.hash(state); }
Self::B { b: self_0 } => { self_0.hash(state); }
Self::C(self_0) => { self_0.hash(state); }
}
}
}
```

### Ignoring

The `#[hash(skip)]` attribute can be used to ignore fields, a whole struct or enum variants in the expansion.

Note that if you also implement the `Eq` or `PartialEq` traits, fields marked with `#[eq(skip)]` or `#[partial_eq(skip)]`
will be ignored during hashing. This is done so that this property holds:

```txt
k1 == k2 -> hash(k1) == hash(k2)
```
That is [expected](https://doc.rust-lang.org/std/hash/trait.Hash.html#hash-and-eq) from `Hash` implementations.

```rust
# use derive_more::Hash;
#
#[derive(Debug)]
struct NoHash; // doesn't implement `Hash`

#[derive(Debug, Hash)]
struct Foo {
num: i32,
#[hash(skip)] // or #[hash(ignore)]
ignored: f32,
}

#[derive(Debug, Hash)]
// Makes all fields of this struct being ignored.
#[hash(skip)] // or #[hash(ignore)]
struct Bar(f32, NoHash);

#[derive(Debug, Hash)]
enum Enum {
Foo(i32, #[hash(skip)] NoHash),
#[hash(skip)]
Bar(NoHash),
Baz,
}
```

This generates code equivalent to:

```rust
# use derive_more::core::hash::{Hash, Hasher};
# struct NoHash;
#
# struct Foo { num: i32, ignored: f32 }
#
impl Hash for Foo {
fn hash<H: Hasher>(&self, state: &mut H) {
match self {
Self { num: self_0, .. } => { self_0.hash(state); }
}
}
}

# struct Bar(i32, NoHash);
#
impl Hash for Bar {
fn hash<H: Hasher>(&self, _state: &mut H) {}
}

# enum Enum {
# Foo(i32, NoHash),
# Bar(NoHash),
# Baz,
# }
#
impl Hash for Enum {
fn hash<H: Hasher>(&self, state: &mut H) {
std::mem::discriminant(self).hash(state);
match self {
Self::Foo(self_0, _) => { self_0.hash(state); }
Self::Bar(_) => {}
Self::Baz => {}
}
}
}
```

### Custom hash function

The `#[hash(with(function))]` attribute can be used to specify a custom hash function for a field.
The function must have the signature `fn<H: Hasher>(value: &FieldType, state: &mut H)`.

```rust
# use derive_more::Hash;
mod my_hash {
use core::hash::Hasher;

pub fn hash_abs<H: Hasher>(value: &i32, state: &mut H) {
state.write_i32(value.abs());
}
}

#[derive(Hash)]
struct Foo {
#[hash(with(my_hash::hash_abs))]
value: i32,
}
```

This generates code equivalent to:

```rust
# use derive_more::core::hash::{Hash, Hasher};
# mod my_hash {
# use derive_more::core::hash::Hasher;
#
# pub fn hash_abs<H: Hasher>(value: &i32, state: &mut H) {
# state.write_i32(value.abs());
# }
# }
#
# struct Foo {
# value: i32,
# }
#
impl Hash for Foo {
fn hash<H: Hasher>(&self, state: &mut H) {
match self {
Self { value: self_0 } => {
my_hash::hash_abs(self_0, state);
}
}
}
}
```

This is useful for types that don't implement `Hash` but can be hashed in a custom way, or when you need different hashing behavior than the default.
Loading