Variadic generics - pre-RFC

Mostly to check I’m following the meaning of * correctly, these are all functionally equivalent function signatures, correct?

fn foo(a: u32, b: u32)
fn foo(a: u32, *(b,): (u32,))
fn foo(*(a, b): (u32, u32))

And this is different in that the type of b will be (u32,) instead of u32, but externally the function will appear to have the same signature?

fn foo(a: u32, *b: (u32,))

It has been deprecated in favor of 0..=20 and this has nothing to do with the intended syntax for variadics ...Stuff/Stuff...

2 Likes

Yes.

Yes, it's deprecated but it's still there in stable, and will not be removed.

I can think of uses for variadics in match patterns, so it does have to do with variadics.

Ok, in that case I see your point.

I would expect to have to type foo(..arg: i32), not ..arg: (i32;). I’m not sure if that would mean it would need to desugar into something else, but.

Sorry, I’m a little confused about this. ^^ How would I create a pull request, without even having a fork?

Not really, this could be removed after the 2021 edition.

What I mean is there's no syntactical conflict between the range usage a...b and the variadic usage ...X. It makes no sense to interpret a...b which unfolds either a or b. The parser could unambiguously interpret $expr ... $expr as a range pattern, and ... $tt as an unfold.

2 Likes

We don't know when the next edition will be, it hasn't been officially announced yet, (to the best of my knowledge).

Ok, I see what you mean.

I’m not thrilled with the semicolon thingy because it seems not fully general – e.g. what if you want the arguments to alternate between types implementing Foo and types implementing Bar? I suppose you could write a custom trait for the purpose, but still.

2 Likes

Are these really tuples, or is it better to use a word like “parameter pack” (à la C++)? I recall discussion from last time about whether you can put something that doesn’t work in a tuple into such a thing – for example, (str, str) isn’t a valid tuple.

On the asterisk: I’d definitely like to see something more similar between use and declaration. Certainly the C++ syntax is meh, but if use is [...arg], then declaring it as (...arg: T) or something seems better than declaring it as (*arg: T).

Macros have a way to specify a separator. Can you do something like sum up the values in a pack by separating with + instead of with ,?

Can you show some examples of multiple packs? For example, can I do something like

fn tuplezip<...T, ...U>(x: (...T), y: (...U)) -> (...(T, U));

Which makes me think that maybe there’s a way to do this that can borrow from existing macro_rules syntax…

You are correct, using this syntax you cannot accept a tuple with alternating types, yet I expected something like this will never be required. You can emulate it using a tuple of pairs. Instead of (u32, i32, u32, i32, ...) you could use ((u32, i32), (u32, i32), ...), which could be represented using ((u32,i32);).

Do you mean "can't be instantiated" with "isn't a valid tuple"?

Considering that you can use .. on variadics without even using the asterisk operator, I disagree. See my answer for tuplezip below. I used variadic generics without the asterisk.

If you have a function signature: fn foo<T1, T2, ..., Tn, *T>(). which is then called with foo<T1, T2, ..., Tn, S1, S2, ..., Sm>(). The asterisk just converts <T1, T2, ..., Tn, S1, S2, ...., Sm> to <T1, T2, ..., Tn, (S1, ..., Sm)>.

Not currently, yet I think we should add something similar to fold expressions from C++, which would be able to do this. You could still [..arg].iter().sum() or write a recursive function.

As every asterisk-type has to be the last type, there can only be one asterisk-type. But you can still do this:

fn tuplezip<T: (X;X), U: (X;X)>(t: T, u: U) -> (..T, ..U) { (..t, ..u) }

At the moment, we have the following situation:

fn() -> R        : Fn<(), Output=R>
fn(A) -> R       : Fn<(A,), Output=R>
fn(A,B) -> R     : Fn<(A,B), Output=R>
fn(A,B,C) -> R   : Fn<(A,B,C), Output=R>
fn(A,B,C,D) -> R : Fn<(A,B,C,D), Output=R>

The main problem with this approach is that there is no way to write, for instance:

struct ReturnFirstArg;

impl<T> FnOnce<T> for ReturnFirstArg where ??? {
    type Output = ???;
    extern "rust-call" fn call_once(self, args: T) -> Self::Output {
        ???
    }
}

Encoding tuples in the following way can help solve this problem:

()            -> ()
(A,)          -> (A, ())
(A, B)        -> (A, (B, ()))
(A, B, C)     -> (A, (B, (C, ())))
(A, B, C, D)  -> (A, (B, (C, (D, ()))))

Then we have the following for function types

fn() -> R        : Fn<(), Output=R>
fn(A) -> R       : Fn<(A, ()), Output=R>
fn(A,B) -> R     : Fn<(A, (B, ())), Output=R>
fn(A,B,C) -> R   : Fn<(A, (B, (C,()))), Output=R>
fn(A,B,C,D) -> R : Fn<(A, (B, (C, (D, ())))), Output=R>

And existing syntax can write the ReturnFirstArg function above as follows:

struct ReturnFirstArg;

impl<T, Args> FnOnce<(T, Args)> for ReturnFirstArg {
    type Output = T;
    extern "rust-call" fn call_once(self, (ret, _): (T, Args)) -> Self::Output {
        ret
    }
}

I believe this technique is quite general; for instance:

struct Sum;

impl<T> FnOnce<(T, ())> for Sum {
    type Output = T;
    extern "rust-call" fn call_once(self, (t, ()): (T, ())) -> T { t }
}

impl<T, Args> FnOnce<(T, Args)> for Sum
    where Sum: FnOnce<Args, Output=T>,
          T: Add<Output=T>
{
    type Output = T;
    extern "rust-call" fn call_once(self, (t, args): (T, Args)) -> Self::Output {
        t + <Sum as FnOnce<Args>>::call_once(Sum, args)
    }
}

This could be applied to regular tuples as well, though that would require syntax changes - simply changing the signature of the Fn traits would not.

1 Like

See also frunk::HList

(Two useful blog posts: about, plucking)

impl<T, ...Args> FnOnce<T, ...Args> for ReturnFirstArg {
    type Output = T;
    extern "rust-call" fn call_once(self, (first, _): (T, ...Args)) -> Self::Output {
        first
    }
}
1 Like

I see I was misinterpreted here, sorry! I was merely providing another approach to the problem without additional syntax - so that variadics are not required. Of course this proposal works too!

Ok, sorry. But on that note, making the Fn* traits better is only one of the goals of this RFC, another goal is variable length arguments for functions, which your scheme can’t handle. Also it is very unergonomic to need to use nested tuples for implementing the Fn* traits, and introduces a size overhead because the elements can’t be reordered to minimize size.

1 Like

done: PR by memoryleak47 · Pull Request #2 · memoryleak47/variadic-generics · GitHub

As the majority misliked the .. and * syntaxes and recommended to unify them. Both .. and * have now been replaced by ... in the repo. You would now write:

fn tuplezip<T: (X;X), U: (X;X)>(t: T, u: U) { (...t, ...u) }

Edit: fixed the example.

2 Likes