Skip to content

Add custom comparators#1182

Merged
taiki-e merged 8 commits intocrossbeam-rs:masterfrom
matthew-mcallister:comparators
Feb 22, 2026
Merged

Add custom comparators#1182
taiki-e merged 8 commits intocrossbeam-rs:masterfrom
matthew-mcallister:comparators

Conversation

@matthew-mcallister
Copy link
Copy Markdown
Contributor

@matthew-mcallister matthew-mcallister commented Feb 24, 2025

Closes #1180.

The diff is large due to having to add a type parameter C and update constraints in numerous places, but the logic changes are not large.

This trait adds the Comparator and Equivalator traits, which generalize the Comparable and Equivalent traits by taking a self parameter in addition to the two keys to compare, making it possible to change the comparison function used without changing the key type.

Summary

  • Adds trait Comparator<L, R> and trait Equivalator<L, R>.
  • Adds struct BasicComparator, which is a zero-sized type that implements Comparator using the Comparable trait, which itself falls back on Borrow and Ord.
  • Adds a type parameter C to SkipList, SkipMap, and SkipSet.
    • C is defaulted to OrdComparator. This preserves the existing behavior of functions like get(), insert(), etc.
  • Adds SkipList::with_comparator(), SkipSet::with_comparator(), and SkipMap::with_comparator() constructors.
    • SkipList::new(), SkipMap::new(), and SkipSet::new() always use C = OrdComparator. It would be nice if it worked for any C that implements Default, but that would break type inference. This is exactly the reason std::collections::HashMap::new() always uses S = RandomState for its defaulted parameter even if S implements Default.
  • Adds a test using a wrapper around Box<dyn Fn(&[u8], &[u8]) -> Ordering> as the comparator.

Hashing

Because the goal of these traits is to support totally different comparison operations from the default for the underlying key type, Equivalator explicitly does not require two equivalent keys to have the same hash. If someone, someday wants to support Equivalator on their hash map, they will have to generalize the Hash trait as well.

Safety

My one minor question is whether unsafe impl<Q, R, K, V, C> Send for RefRange<'_, Q, R, K, V, C> should require C: Sync. I don't think so because it doesn't require K or V to be sync, but it was enough to make me second-guess myself.

Comment thread crossbeam-skiplist/tests/base.rs Outdated
@taiki-e
Copy link
Copy Markdown
Member

taiki-e commented Feb 25, 2025

cc @al8n

@matthew-mcallister
Copy link
Copy Markdown
Contributor Author

Restored the Equivalent and Comparable traits. Now there is no change in behavior compared to master unless you explicitly define your own comparator type.

@matthew-mcallister matthew-mcallister force-pushed the comparators branch 2 times, most recently from 4e63330 to b19ede6 Compare February 27, 2025 18:59
Renamed OrdComparator to BasicComparator and made compatible with the
Equivalent/Comparable traits
@matthew-mcallister
Copy link
Copy Markdown
Contributor Author

It's been a while since I circled back to this, but is there a sentiment as to whether to merge?

Copy link
Copy Markdown

@agavra agavra left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This looks good to me, though I admit I'm a little sketched out by the concept of breaking the hash/eq dependency definition with Equivalator trait...

Would be good to get some committers reviewing this as well! Excited for this feature.

Comment thread crossbeam-skiplist/src/comparator.rs Outdated
Comment on lines +16 to +19
/// Because you can use `Equivalator` to implement very different comparison
/// logic from the `Eq` trait for a given type, there is no expectation that
/// two keys should have the same hash for them to compare equal. For example,
/// this is a valid implementation that treats two inputs of any type as equal:
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It would be good to spell out the motivations for this. It is always possible for the user to implement some funky transform of their keys before they insert into a set/map and when retrieving data as a workaround for not having this. (e.g. in your test, I think a much better implementation if the user wanted that is to just set.insert(key.length()) instead of doing something convoluted with the eq function). That being said, you could still have a comparator that sorts based on key.length and considers two keys equivalent for a sort order if they have the same length.

On the other hand, if we introduce this, it would be impossible to leverage traditional hash structures as optimizations in the future (e.g. add a bloom filter on top of a skiplist) because two elements could be equivalent under this equivilator but have different hashes.

It generally seems dangerous to me to break the standard hash/eq dependency that is implicitly used by so many data structures.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It would be good to spell out the motivations for this.

Agreed. My example was pretty contrived; I can provide more realistic motivation.

Though I will remark that set.insert(key.length()) would produce a totally different program as you'd lose access to the first key inserted. Even skip_map.insert(key.length(), key) is not equivalent since that would give you the last key inserted instead of the first.

It generally seems dangerous to me to break the standard hash/eq dependency that is implicitly used by so many data structures.

Well, this doesn't "break" the Hash/Eq dependency since nothing changes if you stick to the standard library traits. But I think the documentation here may be confusing because hashing was out of scope for this PR, so there is no indication of how you would handle hashing when you need it. In reality, hashing is very easy to accommodate; it just requires another "Hash"-ator trait to go alongside Comparator and Equivalator. This is another concept that has been in the C++ standard library for decades.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Got it! If there's a good reference implementation in the standard C++ library that gives me a lot more confidence in the approach. Thanks for the explanation.

}

/// Returns an iterator over all entries in the map,
/// sorted by key.
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
/// sorted by key.
/// sorted by the comparator's ordering of the key.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should also update SkipSet and change all method docs to use the term "equivalent" (instead of equal).

Comment thread crossbeam-skiplist/tests/set.rs
Addressing feedback on the PR.
- Clarify the lack of an Equivalator-compatible hashing trait.
- More prominently mention comparators in the SkipList/SkipMap docs.
- Updated tests and example to be more realistic and less constrived.
@matthew-mcallister
Copy link
Copy Markdown
Contributor Author

Sorry it took me an eternity to circle back round to this. I got busy and I'm bad at multitasking.

I pushed a commit that addresses the last round of feedback with respect to comments and tests/examples. Are there any maintainers that want to provide feedback and give a disposition as to whether there is a desire to merge this? Or whether we should perhaps take a different approach, such as spinning out the traits into a new crate and adding a hashing trait for DB folks who want to support this with their Bloom filters?

@al8n
Copy link
Copy Markdown
Contributor

al8n commented Feb 14, 2026

Hi @taiki-e, any chances to get this PR merge?

Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR adds support for custom comparators to the skip list data structures in crossbeam-skiplist, addressing issue #1180. The feature enables runtime-configurable comparison functions for ordering keys, which is useful for use cases like ordered K/V stores that need custom comparison logic.

Changes:

  • Introduces Comparator and Equivalator traits to generalize key comparison, with BasicComparator providing the default behavior
  • Adds a type parameter C (defaulting to BasicComparator) to SkipList, SkipMap, and SkipSet, along with with_comparator() constructors
  • Updates trait bounds throughout the codebase from K: Ord to C: Comparator<K> and from K: Comparable<Q> to C: Comparator<K, Q>

Reviewed changes

Copilot reviewed 8 out of 8 changed files in this pull request and generated 4 comments.

Show a summary per file
File Description
crossbeam-skiplist/src/comparator.rs Defines new Equivalator and Comparator traits and BasicComparator implementation
crossbeam-skiplist/src/base.rs Adds comparator type parameter to SkipList and updates all comparison operations to use the comparator
crossbeam-skiplist/src/map.rs Adds comparator type parameter to SkipMap and reorganizes impl blocks based on trait bounds
crossbeam-skiplist/src/set.rs Adds comparator type parameter to SkipSet with similar reorganization
crossbeam-skiplist/src/lib.rs Exports the new comparator module
crossbeam-skiplist/tests/map.rs Adds test demonstrating case-sensitive/insensitive string comparison
crossbeam-skiplist/tests/set.rs Adds test demonstrating dynamic comparator with byte slice encoding
Cargo.toml Allows additional clippy lints for complex types
Comments suppressed due to low confidence (3)

crossbeam-skiplist/src/map.rs:219

  • There's a typo in the documentation: "Print then numbers" should be "Print the numbers".
    /// // Print then numbers from least to greatest

crossbeam-skiplist/src/base.rs:2126

  • The Send and Sync implementations for RefRange should explicitly require C: Send + Sync for clarity, even though these bounds are implicitly enforced through the requirement that &'a SkipList<K, V, C> must be sendable/shareable. Making the bounds explicit would make the code more self-documenting and easier to understand. For example: unsafe impl<Q, R, K, V, C> Send for RefRange<'_, Q, R, K, V, C> where C: Comparator<K> + Comparator<K, Q> + Send + Sync, ...
unsafe impl<Q, R, K, V, C> Send for RefRange<'_, Q, R, K, V, C>
where
    C: Comparator<K> + Comparator<K, Q>,
    R: RangeBounds<Q>,
    Q: ?Sized,
{
}

unsafe impl<Q, R, K, V, C> Sync for RefRange<'_, Q, R, K, V, C>
where
    C: Comparator<K> + Comparator<K, Q>,
    R: RangeBounds<Q>,
    Q: ?Sized,
{
}

crossbeam-skiplist/src/set.rs:46

  • The PR description mentions that the comparator type parameter C is "defaulted to OrdComparator", but the actual code uses BasicComparator instead. This is a discrepancy between the PR description and the implementation. The description should be updated to say BasicComparator to match the actual code.
    /// Returns a new, empty map with the given comparator.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread crossbeam-skiplist/src/set.rs Outdated
Comment thread crossbeam-skiplist/src/set.rs Outdated
Comment thread crossbeam-skiplist/src/map.rs Outdated
Comment thread crossbeam-skiplist/src/map.rs Outdated
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 8 out of 8 changed files in this pull request and generated no new comments.

Comments suppressed due to low confidence (1)

crossbeam-skiplist/src/base.rs:2125

  • The unsafe impl Send and unsafe impl Sync for RefRange should explicitly require C: Send and C: Sync respectively for clarity and safety. While the current implementation is technically safe because RefRange contains &'a SkipList<K, V, C>, and SkipList is only Sync when C: Sync, making the bounds explicit would be clearer and more maintainable. Consider adding C: Send to the Send impl and C: Sync to the Sync impl.
unsafe impl<Q, R, K, V, C> Send for RefRange<'_, Q, R, K, V, C>
where
    C: Comparator<K> + Comparator<K, Q>,
    R: RangeBounds<Q>,
    Q: ?Sized,
{
}

unsafe impl<Q, R, K, V, C> Sync for RefRange<'_, Q, R, K, V, C>
where
    C: Comparator<K> + Comparator<K, Q>,
    R: RangeBounds<Q>,
    Q: ?Sized,
{
}

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Copy link
Copy Markdown
Member

@taiki-e taiki-e left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM, thanks @matthew-mcallister!

As for Send/Sync, the existing implementations are already questionable, so I'll address on our end before release.

As for documentation, PRs for further improvements are welcome.

@taiki-e taiki-e merged commit a404b7b into crossbeam-rs:master Feb 22, 2026
37 checks passed
@taiki-e taiki-e mentioned this pull request Feb 22, 2026
1 task
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Development

Successfully merging this pull request may close these issues.

Custom comparators for SkipList

5 participants