Generics which are private to a type

I occasionally run into an issue where I need a struct to be generic, but I don't actually need that generic type to show outside of the type (i.e. it is internal to the type). This is usually where you are gluing together two other generic structs, and you need to make sure that the types from the pieces fit together properly:

struct A<T> {
     func foo(_ a:InType) -> T {...}
}

struct B<T> {
    func bar(_ b:T) -> OutType {...}
}

struct C<T> {
    var a:A<T>
    var b:B<T>

    func foobar(_ inValue:InType) -> OutType {
        b.bar(a.foo(inValue))
    }
}

(Note: This is simplified. Most actual use-cases have the struct wrapping a protocol)

It is a shame that you can't have collections over C with different T's, since T is irrelevant to the interface of C. Similarly, if you want to store C in a variable, T tends to leak out even when you really want it to be private.

It would be nice to be able to mark a generic type as "private" and have the compiler track it, but treat it externally as just C (without the T). Much easier than the type erasure tricks, and might take a lot of pressure off of the need for type erasure in general.

struct C<private T> {
    private var a:A<T>
    private var b:B<T>

    init<U>(a:A<U>, b:B<U>) where T == U {...}

    func foobar(_ inValue:InType) -> OutType {
        b.bar(a.foo(inValue))
    }
}

T would be required to be defined by the init somehow, but otherwise not allowed in any public signature.

How difficult would it be to add something like this to the compiler?

1 Like

As a concrete example of how this is useful, imagine that I have a protocol with an associated type. With private generics, I can create a quick type erasing wrapper around it with MUCH less boilerplate... and it forces me away from the problematic edge cases where the erased type would leak out (with an understandable error message that T is private).

protocol Proto {
    associatedtype X

    func getX()->X
    ///...
}

///Wrapper exposes X, but erases T
struct Wrapper<private T:Proto, X> {
    private var store:T

    init<P:Proto>(_ proto:P) where P == T, X == P.X {
        self.store = proto
    }

    func getX() -> X {
       self.store.getX()
    }
    ///...
}
1 Like

I'm not sure what you're trying to achieve, but C<T1> and C<T2> have different memory layouts. It's not possible to just lay them bare in an array, and you can not write

let a = Wrapper(proto1) // type Wrapper<X>

// This *should* work from type system perspective,
// but cannot because `Wrapper<T1, X>` and `Wrapper<T2, X>`
// have different layout.
a = Wrapper(proto2) // type Wrapper<X>

That's essentially why you need existential to box T or even C<T>. Hiding information about T doesn't change that fact unless you include type erasure or some kind of boxing mechanism into that hiding process.

If boxing is part of your plan, there are some idea floating around about synthesizing type erasure. You can probably search this forum for that. There's also this thread (Lifting the "Self or associated type" constraint on existentials) that you may be interested in. It's already 2-yr old though, so best not to bump it.

Thanks. Quick question:

If I have 2 different structs that adhere to the same protocol, I can make an array of that protocol, even though they are different underlying types. How does that work?

C<T1> and C<T2> are different types, and the compiler should be aware of that fact internally, but it should also allow them to be treated polymorphically.

I don't know enough about the compiler internals to speak to the exact mechanism (I am willing to learn if someone is willing to teach), but the behavior for the end user would be similar to how different types adhering to a protocol can be mixed. Maybe there is a thunk/box that the compiler creates to keep it straight, but the end user shouldn't have to worry about it...

They put it in a box (called protocol witness table existential container). It's kinda similar to a type erasure, but at a much lower level and likely more efficient.

This WWDC Understand Swift Performance should be a pretty decent high-level primer. Protocol witness table is in the latter half.

No idea if you wanna get your hand dirty (on the implementation) though :woman_shrugging:. You'll need to ask someone else.

1 Like

I too am not sure about what exactly you're trying to achieve. You say "private" generics, but I don't understand to whom they're private.

Anyway, if you're looking at this from an API author's perspective, it might be interesting to take a look at this pitch: Proposing to expand `available` to introduce `discouraged`. So in your case, with this feature or with a feature that hides underscore-prefixed types and declarations you could do:

struct C<_T> {

    ...

}

// or

struct C<
  @available(discouraged: "Not meant for public use") T
> {

    ...

}