9

This is similar to How do I use a custom comparator function with BTreeSet? however in my case I won't know the sorting criteria until runtime. The possible criteria are extensive and can't be hard-coded (think something like sort by distance to target or sort by specific bytes in a payload or combination thereof). The sorting criteria won't change after the map/set is created.

The only alternatives I see are:

  • use a Vec, but log(n) inserts and deletes are crucial
  • wrap each of the elements with the sorting criteria (directly or indirectly), but that seems wasteful

This is possible with standard C++ containers std::map/std::set but doesn't seem possible with Rust's BTreeMap/BTreeSet. Is there an alternative in the standard library or in another crate that can do this? Or will I have to implement this myself?


My use-case is a database-like system where elements in the set are defined by a schema, like:

Element {
    FIELD x: f32
    FIELD y: f32
    FIELD z: i64

    ORDERBY z
}

But since the schema is user-defined at runtime, the elements are stored in a set of bytes (BTreeSet<Vec<u8>>). Likewise the order of the elements is user-defined. So the comparator I would give to BTreeSet would look like |a, b| schema.cmp(a, b). Hard-coded, the above example may look something like:

fn cmp(a: &Vec<u8>, b: &Vec<u8>) -> Ordering {
    let a_field = self.get_field(a, 2).as_i64();
    let b_field = self.get_field(b, 2).as_i64();
    a_field.cmp(b_field)
}
kmdreko
  • 42,554
  • 6
  • 57
  • 106
  • Could you give examples to illustrate the breadth of the types of sorting functions that you need to configure? – Peter Hall Jan 01 '21 at 23:13
  • @PeterHall I've elaborated on my specific use-case – kmdreko Jan 01 '21 at 23:43
  • 3
    This is a really interesting question. You could put a `dyn Fn(T, T) -> Ordering` into each wrapper, but that would add a bunch of overhead. I don't know of a way to make `BTreeMap` dynamically dispatched without per-key overhead. – ddulaney Jan 02 '21 at 04:56
  • Why do you find wrapping the elements wasteful? – EvilTak Jan 02 '21 at 06:45
  • @EvilTak *wrapping the elements with the sorting criteria* means each one would need some kind of reference to the schema, but since all elements would reference the same thing it seems like a waste – kmdreko Jan 02 '21 at 17:25
  • Does this answer your question? [How do I use a custom comparator function with BTreeSet?](https://stackoverflow.com/questions/34028324/how-do-i-use-a-custom-comparator-function-with-btreeset) – Chayim Friedman Dec 19 '21 at 00:57
  • @ChayimFriedman unfortunately no, I've seen and linked that already. It doesn't really cover the comparator being runtime-defined and being different per map instance of the same type. – kmdreko Dec 19 '21 at 01:04
  • @eggyal sure, in your example `compfn` is zero-sized. However, from the title and surrounding context, how it is sorted in my use-case is based on a *runtime value* which will require a function object that holds at least some state, which will be duplicated for each element if I follow your solution. – kmdreko Dec 29 '22 at 03:43
  • 1
    I published [copse](https://crates.io/crates/copse) which ports stdlib's BTree collections to add this functionality. – eggyal Dec 31 '22 at 01:48
  • @eggyal if you post an answer with that, I would accept it. I've tried it out and it works exactly as I would hope. – kmdreko Feb 03 '23 at 16:15

2 Answers2

0

Would it be possible to pass the comparator closure as an argument to each node operation that needs it? It would be owned by the tree wrapper instead of cloned in every node.

Mutant Bob
  • 3,121
  • 2
  • 27
  • 52
0

There is the copse1 crate written by @eggyal which provides BTreeSet, BTreeMap, and BinaryHeap types mimicking the standard library but which allow a ordering to be specified separately.

Here is an example BTreeSet of Points that are ordered by a distance to a fixed point:

use copse::{BTreeSet, SortableBy, TotalOrder};

struct Point {
    x: f64,
    y: f64,
}

impl Point {
    fn distance(&self, other: &Point) -> f64 {
        let dx = self.x - other.x;
        let dy = self.y - other.y;
        (dx * dx + dy * dy).sqrt()
    }
}

struct PointTotalOrder(Point);

impl TotalOrder for PointTotalOrder {
    type OrderedType = Point;

    fn cmp(&self, this: &Point, that: &Point) -> std::cmp::Ordering {
        let this_dist = self.0.distance(this);
        let that_dist = self.0.distance(that);
        f64::total_cmp(&this_dist, &that_dist)
    }
}

impl SortableBy<PointTotalOrder> for Point {
    fn sort_key(&self) -> &Point {
        self
    }
}

fn main() {
    let fixed = Point { x: 2.0, y: 3.0 };

    let mut set = BTreeSet::new(PointTotalOrder(fixed));
    set.insert(Point { x: 0.0, y: 0.0 });
    set.insert(Point { x: 3.0, y: 3.0 });
    set.insert(Point { x: 2.0, y: 5.0 });

    for point in set {
        println!("({}, {})", point.x, point.y);
    }
}
(3, 3)
(2, 5)
(0, 0)

1. "Copse" is a word meaning "a small group of trees" which I find very fitting.

kmdreko
  • 42,554
  • 6
  • 57
  • 106