Specifying generic const params without turbofish?

I have a function like so

/// Maps a SmallVec into another, potentially with a different size
fn filter_map_to_different_size<T, U, F: Fn(T) -> Option<U>, const N: usize, const N2: usize>(
    v: SmallVec<[T; N]>,
    f: F,
) -> SmallVec<[U; N2]> {
    v.into_iter().filter_map(f).collect()
}

Calling this function is a little clumsy. I either have to do so in a context where the new size can be inferred, or call it like filter_map_to_different_size::<_, _, _, _, 4>(v, f).

Is there a pattern that would allow this to be called without the turbofish bloat? Perhaps as filter_map_to_different_size(v, f, 4)?

I tried something like this, but obviously it's not valid. Is this likely to happen in the future?

fn filter_map_to_different_size<T, U, F: Fn(T) -> Option<U>, const N: usize, const N2: usize>(
    v: SmallVec<[T; N]>,
    f: F,
    const _n: N2,
) -> SmallVec<[U; N2]> {
    v.into_iter().filter_map(f).collect()
}
2 Likes

Could you provide a more realistic example of the problem? I think the best design would depend on what you're trying to achieve, and may well mean ditching the const generics entirely.

Part of the issue is that const generic parameters are currently not inferred, even in trivial cases like an array literal.

Would interleaving work?

Previously, type parameters were required to come before all const parameters. That restriction has been relaxed and you can now interleave them.

1 Like

No because you still need to specify all parameters unless they're defaulted.

1 Like
use smallvec::{smallvec, SmallVec};

const N: usize = 5;
const N2: usize = 5;

fn map_to_different_size<T, U, F: Fn(T) -> U>(v: SmallVec<[T; N]>, f: F) -> SmallVec<[U; N2]> {
    v.into_iter().map(f).collect()
}

fn main() {
    let v: SmallVec<[u16; N]> = smallvec![0, 1, 2, 3, 4];
    map_to_different_size(v, |x| x as u32);
    let v: SmallVec<[u8; N]> = smallvec![0, 1, 2, 3, 4];
    map_to_different_size(v, |x| x as u32);
}

Playground works fine.
Upd.: Oh sorry that's not the correct example.

I've come across this issue in a few different shapes, which is why I posted that general example. But here's a more realistic recreation of where it came up this time.

struct Container<T, const N: usize> {
    vec: SmallVec<[T; N]>
} 
impl<T, const N: usize> Container<T, N> {
    fn filter_map<U, F: Fn(T) -> Option<U>, const N2: usize>(self, f: F) -> Container<U, N2> {
        Container { vec: self.vec.into_iter().filter_map(f).collect() }
    }
}

I have some container type, backed by SmallVec. I'm doing transformations on that data into new containers, and need to set the size used by SmallVec for the transformed result.

The solution that I have is functional, it's just ugly because of the turbofish.

I can just do let temp: Container<u32, 4> = container.filter_map(|x| todo!()); so it's not a terrible state of affairs, but I was just curious whether I could do better on API ergonomics.

This uses constant sizes. I need to specify the size at the call site as an argument.

I want to see a working example even with turbofish.

This works.

// Compiles fine
fn main() {
    let container: Container<u16, 5> = Container { vec: smallvec![1, 2, 3, 4, 5] };
    let a = container.filter_map::<_, _, 2>(|x| if x < 3 { Some(u32::from(x)) } else { None });
}

It requires the const_generics feature of SmallVec.

1 Like

I'd say you should just use the same return size N in the output container as in the input one. You're overcomplicating the type signature. SmallVec is usually used with some small buffer size, often just N=1 or N=2. If you set the buffer size too large, then it is easy to get poor performance due to lots of copies of a large, maybe mostly empty buffer.

Thus I ask: do you really need to regularly vary the output container size? Or maybe you can just pass the same N around? Remember that in an off case where you really need a different smallvec, you can always insert a manual conversion. Depending on your code structure, it can easily be both faster and more readable.

I do need the current behaviour, yes. The real data containers that I'm building enforce some relatively complex invariants and are quite carefully optimised. I am happy with the functionality that I have now, this question is just about tidying up the API to make it look a little nicer.

I expect that the answer is that it's currently impossible to avoid the turbofish in this case.

Hmm... Notice that N2 is placed in the return type. It means the compiler can infer its type from a lvalue of an assignment

fn infer_me<A,B>(input:A) -> B
where 
    B: From<A>
{
    return input.into()
}

fn main() {
    let a = 10i32;
    let b:i64 = infer_me(a);
   // same : let b = infer_me<_,i64>(a);
    println!("{}",b);

in your case

let l_value: SmallVec<[_;10]> = filter_map_to_different_size(v,f)

You could try something like this:

pub struct Len<const LEN:usize>;

/// Maps a SmallVec into another, potentially with a different size
fn filter_map_to_different_size<T, U, F: Fn(T) -> Option<U>, const N1: usize, const N2: usize>(
    v: SmallVec<[T; N1]>,
    f: F,
    _: Len<N2>
) -> SmallVec<[U; N2]> {
    v.into_iter().filter_map(f).collect()
}

\\ ... filter_map_to_different_size(v, f, Len::<4>)

1 Like

This topic was automatically closed 90 days after the last reply. We invite you to open a new topic if you have further questions or comments.