How to generalize functions in which their only difference is that they use different fields of a struct?

Simple example:

fn do_sum_a(thing: &Thing) -> i32 {
   let mut sum = 0;
   for sub_thing in thing.things {
       sum += sub_thing.field_a;
   }
   sum
}

fn do_sum_b(thing: &Thing) -> i32 {
   let mut sum = 0;
   for sub_thing in thing.things {
       sum += sub_thing.field_b;
   }
   sum
}

The only difference in these functions is that they are accessing a different field in a struct.

One way would be to pass an enum that states what field you want to sum.

enum WhichField {
  FieldA,
  FieldB
}

fn do_sum(thing: &Thing, which_field: WhichField) -> i32 {
   let mut sum = 0;
   for sub_thing in thing.things {
       match which_field {
          WhichField::FieldA => sum+= sub_thing.field_a,
          WhichField::FieldB => sum+= sub_thing.field_b,
       }
   }
   sum
}

But this would add an unnecessary runtime cost.

Another way would be to use const generics. A const int could do the job, but it's not expressive. Using a user defined enum as a const generic parameter requires the use of an unstable feature:

#![feature(adt_const_params)]

fn do_sum<WHICH_FIELD: WhichField>(thing: &Thing) -> i32 {
   let mut sum = 0;
   for sub_thing in thing.things {
       match WHICH_FIELD {
          WhichField::FieldA => sum+= sub_thing.field_a,
          WhichField::FieldB => sum+= sub_thing.field_b,
       }
   }
   sum
}

Personally I would probably use a closure.

fn do_sum<F: FnMut(&SubThing) -> i32>(thing: &Thing, mut get: F) -> i32 {
    let mut sum = 0;
    for sub_thing in &thing.things {
        sum += get(sub_thing);
    }
    sum
}

It technically leaves more room for misuse than the enum would[1], but realistically that probably doesn't matter much for most APIs


  1. For example, you could return a constant from the closure instead of a field on the struct ↩︎

1 Like

Make the function take a closure returning the field you want.

That's an elegant solution. But there is the runtime cost of running the closure instead of getting the field directly. The only other solution I can come up with is a macro, but I'd rather avoid macros.

In a case this simple the closure will almost certainly be inlined.

It actually looks like the compiler is inlining the entire do_sum function not just the closure in this case. If you really need to care about performance for some reason you'll need to do some benchmarking with code that's less trivial.

3 Likes

Yeah, it probably inlines the entire thing, as it is probably the only way to optimize away the closure. I'm still trying to learn how to understand godbolt output...

Well if the function wasn't optimized out, there would be a new copy of the function for each closure it was called with. That's what monomorphization is. The reasons a closure couldn't be inlined have more to do with what the closure is doing and how it would interact with the code calling the closure.

Since the problem you're solving is "accessing a field on the argument to the closure" I have a hard time thinking of a scenario where the closure wouldn't be inlined[1]


  1. short of passing a trait object instead of a concrete closure ↩︎

2 Likes

For reference, I note that the standard library calls such a closure a "key extraction function" and likely would name do_sum as sum_by_key:
https://doc.rust-lang.org/std/?search=_by_key