Support typealias for generic struct inside a function

Are there any specific reasons why the following code is not allowed?

use std::marker::PhantomData;

struct S<T> {
    _marker: PhantomData<T>
}

fn f<U>() {
    type T = S<U>;
}

fn main() {}

Got an error:

error[E0401]: can't use generic parameters from outer function
  --> src/main.rs:23:16
   |
22 | fn f<U>() {
   |    - - type parameter from outer function
   |    |
   |    try adding a local generic parameter in this method instead
23 |     type T = S<U>;
   |                ^ use of generic parameter from outer function

The general principle behind items (like fn, mod, struct/enum/type, trait, impl) within blocks or function bodies is that they behave essentially the same as if they were moved to the top level, outside of the containing function/method/constant. (The only difference is their visibility; you cannot name them outside of the block they come from.) This principle is AFAIK also followed for compilation where nested items are in many regards processed independently from the containing item.

There are already certain kinds of “exceptions” to this principle: In impl blocks, you have the type Self which behaves essentially like a type synonym, but its definition can effectively contain some arguments. And AFAICT closures seem rather magical, too, in their ability to work with type or lifetime parameters even though they are essentially (equivalent to) defining some anonymous structs that implement Fn* traits.

So while allowing uage of generic parameters in nested item definitions is certainly a usesful feature idea, there are 3 kinds of reasons that could be raised against it, those being

  • the compiler couldn't process the nested item independently anymore, which might be relevant for performance. (That’s just a guess, it might be a nonproblem..)
  • the rationale to allow nested items inside blocks is currently just that top-level items happen to be liberally allowed in more places than just in modules. This is just a straightforward restriction of a limitation, and not necessarily intended to be the much bigger and more involved feature it would have to do if using outer generic parameters was allowed
  • while the case of type aliases might still be straightforward, any more general attempt to allow using outer generic arguments would quickly run into lots of nontrivial details to considere. E.g. to name some example off the top of my head: currently, you can figure out whether Sometype: 'a by considering whether or not 'b: 'a for all lifetimes syntactically appearing in Sometype. If structs like fn f<T>() { struct Foo(T); } were allowed, then reasoning about Foo: 'a would need to consider T as well. To give another example, reasoning about trait implementations when either the struct or perhaps even the trait involved use generic arguments from outside might get confusing. It could probably all be made to work if all of these cases would be effectively translated into generic structs or traits after all, but this leads back to the previous point, that all of this would become a quite involved feature, whereas nested items are currently not supposed to be a particularly complex or special thing

TL;DR, depending on what exactly you’d want to allow or disallow, whether it’s just usage of outer generic arguments in type aliases, or also in struct, trait, impl … for …, etc…, there’s no strong reason agains allowing any of this AFAICT, but making this work, particularly becond type aliases, could be a fairly complex language feature, so one would need to discuss whether or not that’s wanted. If only type aliases would gain this ability, one could question if that’s too inconsistent.


I also would be surprised if there isn’t any previousl discussion on this question, but I’m not personally recalling any discussion; if someone else finds or remembers previous discussion, feel free to post it here.

4 Likes

I remember trying to get something similar working not long ago, when I wanted to make a generic version of this code: rust - How do I allocate a Vec<u8> that is aligned to the size of the cache line? - Stack Overflow

unsafe fn aligned_vec<const N: usize>(n_bytes: usize) -> Vec<u8> {
    #[repr(C, align(N))] // <- incorrect attribute format: `align` takes exactly one argument in parentheses
    struct Align([u8; N]); // <- use of generic parameter from outer function

   /* snip */
}

This caused two problems: the one described in this thread, and another due to the fact that macros are resolved before generics, so in this case the compiler complains that N is not a literal.

Note that this would be possible in C++ :

template<std::size_t N>
auto aligned_vec(std::size_t n_bytes) {
    struct alignas(N) Align {
        uint8_t inner[N];
    };

     /* snip */
}

If it becomes possible to use const-generics from an outer function in an inner struct / function declaration, I think the problem regarding the macros resolution order will become a lot more apparent. (Though, this is still a very niche use-case)