Problem passing functions as arguments and deref

The general consensus seems to be that, when possible, you want to take slices as arguments rather than, say, Vecs. I'm running into a problem where I can't seem to (easily) get that to work when I'm passing functions as arguments, because Rust doesn't "know" to (or that it can) auto-deref the argument to the function I'm passing in.

The core of the problem is illustrated here:

type Bitstring = Vec<bool>;

struct ScoredItem<T> {
    item: T,
    score: i64
}

// The "preferred" argument type, i.e., a slice
fn count_ones(bits: &[bool]) -> i64 {
    bits.iter().filter(|&&bit| bit).count() as i64
}

fn make_scored_item<T>(item: T, scorer: impl Fn(&T) -> i64) -> ScoredItem<T> {
    let score = scorer(&item);
    ScoredItem {
        item,
        score
    }
}

Given a some_bitstring: Bitstring (i.e., Vec<bool>), I'd like to be able to make calls like

make_scored_item(some_bitstring, count_ones)

These fail, however, because the compiler can't match the type of count_ones (which takes the preferable &[bool] argument type) to the actual type &Vec<bool>.

Fix 1: Have count_ones take &Vec<bool>

If I just change count_ones to take &Vec<bool> as its argument, everything is swell. But that runs counter to the prevailing suggestion that I want to take a slice (&[bool]) instead.

Fix 2: A helper function that quietly converts

Another fix is to make a helper function that just passes the call through, "magically fixing things" by converting from &Vec<bool> to &[bool] along the way:

fn count_ones_vec(bits: &Bitstring) -> i64 {
    count_ones(bits)
}

With this I can successfully call make_scored_item(some_bitstring, count_ones_vec). It's annoying to have this around, though, especially since it takes the argument type we were trying to avoid. (It doesn't look like it's actually a performance problem, though, as I think the compiler is inlining it straight away.)

Fix 3: Introduce a closure that quietly converts

Alternatively (and nearly identically), I can introduce a closure that will "magically" perform the necessary conversion:

make_scored_item(some_bitstring, |bits| count_ones(bits))

Having a closure of the form |x| f(x) should in general be replaced by just f, though, and in fact Clippy will usually complain about exactly this thing. It doesn't here, though, because I think it "realizes" that the conversion happening in the closure is important.

Full example

A full example is in the playground. If you uncomment the line:

    // let scored_bits = make_scored_item(make_bitstring(20), count_ones);

the code will no longer compile:

13 | fn count_ones(bits: &[bool]) -> i64 {
   | ----------------------------------- found signature of `for<'r> fn(&'r [bool]) -> _`
...
31 |     let scored_bits = make_scored_bits(20, count_ones);
   |                       ----------------     ^^^^^^^^^^ expected signature of `for<'r> fn(&'r Vec<bool>) -> _`
   |                       |
   |                       required by a bound introduced by this call

Thanks for suggestions

Anyone know a better way to deal with this? I'm fairly new to Rust, so I can totally imagine that there's a simple way to make all this better. I wasn't able to find anything in my searching, although that might have just been trouble figuring out exactly what to search for.

Many thanks – Nic

Fix 4: Add another generic type

fn make_scored_item<T: Borrow<R>, R: ?Sized>(item: T, scorer: impl Fn(&R) -> i64) -> ScoredItem<T> {
    let score = scorer(item.borrow());
    ScoredItem { item, score }
}

https://play.rust-lang.org/?version=stable&mode=debug&edition=2021&gist=b23057dd020dfda53563f4d757b4e1a5

1 Like

Your playground and your first code-block don't line up, and your playground doesn't have the commented line, and the commented line doesn't seem to make sense when put into the playground, so it's sort of hard to follow all this.

However, the fix for your playground is

-fn make_scored_bits(num_bits: usize, scorer: impl Fn(&Vec<bool>) -> i64) -> ScoredBits {
+fn make_scored_bits(num_bits: usize, scorer: impl Fn(&[bool]) -> i64) -> ScoredBits {

Playground.

@quinedot (& possibly @scottmcm): I'm really sorry about the mismatch between the post and the playground. I'd never used the Rust Playground before and I assumed (incorrectly) that subsequent changes I made would be reflected in a link I captured early on. It's clear to me now that this isn't true, so the playground link I had up above just wasn't right.

Many apologies. I've edited the link above to point to the correct playground and I'll link to it here as well.

The intended version of make_scored_bits (which highlights the issue) is:

fn make_scored_item<T>(item: T, scorer: impl Fn(&T) -> i64) -> ScoredItem<T> {
    let score = scorer(&item);
    ScoredItem {
        item,
        score
    }
}

Here I want the &T in the type of scorer so that make_scored_item is generic in the type T. Then I'd like to want to pass count_ones in as the value of that argument, but it doesn't work because count_ones takes &[bool] and the type system is expecting &Bitstring, i.e., &Vec<bool>.

Thus I can't use your fix @quinedot because that hardcodes the type into make_scored_bits, when I want that to be a generic.


@scottmcm Thanks for your fix – I'm not sure I ever would have found/thought of that! I'll admit I was pretty baffled by it just looking at the code, but reading the documentation on the Borrow trait was really helpful and I now get what you're doing there.

Thanks to all for the prompt responses!

2 Likes

Having just incorporated @scottmcm's changes into my (more complex) code base, I'm left wondering if those are actually an "improvement". There's no question that I now understand what was going on better (so thanks!), but I wonder if the "hack" of just introducing closures (what I called Fix 3 above) is a lot easier to understand (or at least a lot less likely to raise lots of questions).

Is this a case where we've run into the (current) limits of the nice sugar that Rust provides to simplify programming in various ways? I (now) understand how the Borrow trait allows use to handle these kinds of type conversions in a very explicit way, but solutions like introducing a closure or the count_ones_vec helper function hide those details at the expense of an apparently meaningless operations.

Would it be possible for the Rust compiler to automagically handle these cases? Or do function types just make it too hard to infer the necessary types and we have to do it ourselves?

Thanks! Makes total sense now (as does tripping over the playground sharing).

Not without new features; type parameters have to resolve to a single type, and these are distinct types. So the tradeoff here is

  • more complicated (generic) function signatures, like the suggestion you applied
    • for more ergonomics at the call site
  • simpler function signature
    • but more complications / typing at the call site

(There's sometimes also the risk of going so generic the callsites get worse because inference fails, but I don't think that's a concern here, since an argument's type drives the rest.)

1 Like