0

I want to write some code that should be generic across implementors of a particular trait, but whose exact type can't be known until runtime.

In particular, I want to apply a function from PartialOrd onto a subset of the cases from a given enum. Here's a basic version of what I'm trying to implement:

enum Value {
    String(String),
    Float(f64),
    Array(Vec<Value>),
}

fn apply_op<T, F>(v: &Value, compare_val: &Value, op: F) -> bool
where
    F: FnOnce(&T, &T) -> bool,
    T: PartialOrd,
{
    match (v, compare_val) {
        (Value::String(s), Value::String(compare_str)) => op(s, compare_str),
        (Value::Float(f), Value::Float(compare_f)) => op(f, compare_f),
        _ => false,
    }
}

And then something like PartialOrd::gt is passed in for op.

Since the type T is determined by the match arm, it can't be monomorphized and therefore won't compile.

Is there a way to get around this, maybe with some kind of trait/wrapper struct finagling?

Playground link here, which includes the version above (which doesn't compile) and the macro approach I'm currently using to get around this. The macro works fine, but it really feels like this should be possible without one.

Chayim Friedman
  • 47,971
  • 5
  • 48
  • 77
  • Seems like the same issue [here](https://stackoverflow.com/questions/72212772/access-the-value-of-an-enum-variant/72220167#72220167). A macro is the only way to do that currently as HKTs are not available in Rust at the moment. – Ian S. May 17 '22 at 19:45

1 Answers1

0

You can't late-bind a generic function, but you can create a trait for that:

pub trait Comparator<T> {
    fn compare(self, a: &T, b: &T) -> bool;
}

Then create a type for each comparator, for example Gt:

struct Gt;
// You can also implement it individually for `String`, `f64` and `Vec<Value>`.
impl<T: PartialOrd> Comparator<T> for Gt {
    fn compare(self, a: &T, b: &T) -> bool {
        PartialOrd::gt(a, b)
    }
}

Then:

fn apply_op<F>(v: &Value, compare_val: &Value, op: F) -> bool
where
    F: Comparator<String> + Comparator<f64> + Comparator<Vec<Value>>,
{
    match (v, compare_val) {
        (Value::String(s), Value::String(compare_str)) => op.compare(s, compare_str),
        (Value::Float(f), Value::Float(compare_f)) => op.compare(f, compare_f),
        _ => false,
    }
}
Chayim Friedman
  • 47,971
  • 5
  • 48
  • 77
  • This was the key! For posterity, this is basically how I ended up implementing it (using an existing `Op` enum instead of a trait): [playground](https://play.rust-lang.org/?version=stable&mode=debug&edition=2021&gist=f5f3c02af430eb0902d4fa3c519c7ba2). Thanks so much! – erik rhodes May 17 '22 at 20:43
  • @erikrhodes Note the `_ => return false` arm is redundant as the other arms already exhaustively match the enum. (I'd even say the arm is harmful as if you were to later hypothetically add a new enum variant, you'd _want_ a compile-time error that you didn't handle it.) – cdhowie May 17 '22 at 20:54
  • @cdhowie Great point -- that's intentional in my actual code where there are other variants that don't map to `PartialOrd` implementations, but totally unnecessary here. I guess the most correct thing to do here is return a `Result` and `Err` on the unsupported match arms. – erik rhodes May 17 '22 at 21:03
  • @erikrhodes Yeah, that makes the most sense IMO as it allows the caller to decide whether or not to panic. – cdhowie May 18 '22 at 02:42