Accepted already: SE-0404.
I'm guessing that's not in Xcode 15b7, then? It seems it was just accepted two weeks ago, and is not yet merged into main.
Grea Idea
It's great to see protocols can be nested in other types has been implemented in SE-0404. Would anyone know if types nested in protocols has been implemented (or if there's a proposal to track it)?
I.e.
protocol Foo {
enum Bar {
}
}
This is not supported and Iâm not aware of any plans to do so, but you can simulate this with a typealias inside the protocol if you like.
If anyone wants to take this on, the main difficulty is that there are two possible interpretationsâeither the nested type captures the protocolâs Self parameter, or it doesnâtâand either interpretation makes sense and has different behavior and use cases. So youâd have to decide which one you want before proceeding with implementation.
I think this feature would benefit greatly from being built on top of another feature: extensions on existentials.
Consider this hypothetical example extending an existential of a protocol to add conformance to the protocol itself (just like how Error behaves today):
protocol AsyncVariable<Value> {
associatedtype Value
func getValue() async throws -> Value
func setValue(to value: Value) async throws
}
struct TypeMismatchError: Error {
var expectedType: Any.Type
var actualType: Any.Type
}
extension any AsyncVariable: AsyncVariable {
func getValue() async throws -> Any {
func `do`<Value>(this: some AsyncVariable<Value>) async throws -> Any {
try await this.getValue()
}
return try await _openExistential(self, do: `do(this:)`)
}
func setValue(to value: Any) async throws {
func `do`<Value>(this: some AsyncVariable<Value>) async throws {
guard let expectedValue = value as? Value else {
throw TypeMismatchError(expectedType: Value.self, actualType: type(of: value))
}
try await this.setValue(to: expectedValue)
}
try await _openExistential(self, do: `do(this:)`)
}
}
@Slava_Pestov, do you think such a feature is feasible or worth the effort to implement? I certainly would greatly appreciate having this powerful tool!
When Iâve really wanted this feature, itâs been for mundane namespacing purposes: an enum, for example, that only applies to one parameter of one protocol method surfaces as a top-level type in an API, where it would really make more sense nested inside the relevant protocol.
(In other words, for anyone lost in that prose:)
// This works:
struct Widget {
func twiffle(direction: TwiffleDirection) { ⌠}
enum TwiffleDirection {
case hither
case yon
}
}
// But this is not currently allowed:
protocol Widget {
func twiffle(direction: TwiffleDirection)
enum TwiffleDirection {
case hither
case yon
}
}
// âŚso instead we have to do this:
protocol Widget {
func twiffle(direction: TwiffleDirection)
}
enum WidgetTwiffleDirection {
case hither
case yon
}
Capturing Self and associated types is interesting and possibly useful, but unnecessary / actively harmful for this use case. (In the example above, one should be able to store a generic Widget.TwiffleDirection value that works for any type conforming to Widget.)
I wonder whether as a first proposal we might provide a syntax that explicitly specifies namespace-only nesting, leaving the capturing case for later? For example:
protocol Widget {
func twiffle(direction: TwiffleDirection)
static enum TwiffleDirection {
case hither
case yon
}
}
Maybe static is the wrong keyword there; itâs taking a page from Java, but in Java IIRC the static specifier for nested types is about capturing a reference to the containing value, not the containing typeâs generic parameters (i.e. capturing self, not Self). Still, maybe thereâs a workable first step somewhere in that design space?
I hadnât considered the possibility of allowing both, but yeah, thereâs no technical reason this couldnât be done. Itâs a subtle distinction to explain to users, though.
The way protocol type aliases work today actually has both behaviors; if the underlying type involves Self, the type alias is only a member of conforming types, otherwise it is also visible as a member of the protocol itself. (However, the only reason we can get away with this is because the âbodyâ of a type alias is simple to analyze; we wouldnât want to walk the body of every method of a nested nominal type to determine if it involves Self or not.)
Yeah. I think my whole idea depends on finding good terminology. Fortunately, naming things is a famously easy problem in computer science. /s
If we allowed nested types inside protocols to avoid capturing Self, is that a path (or a slippery slope, depending on how you feel about the feature) to allowing nested types inside anything to not capture outer generic parameters? It may seem odd to allow it for one kind of declaration but not others.
There's been a rare occasion (rare enough that I don't remember the use case) where I've wanted to be able to have a type nested in a generic type purely for the namespacing aspect, without it capturing any outer context:
struct X<T>
"static" struct Y {} // to borrow the Java keyword again
}
let y: X.Y
But it's never really felt critical enough to push further on.
My gut says yes? It seems like âany nominal type can function as a namespace for tightly related typesâ ought to be a uniformly applied general principle.
In general, both these cases (types in protocols and generic types) seem to have an obvious solution: if the nested type references a generic placeholder or Self, it must be referenced in a fully qualified way, otherwise it doesn't. Was there some edge case that hasn't been mentioned here? I think there's some undesigned syntax for the protocol case where you might need to disambiguate the protocol's Self from the type's, but the generic's case seems pretty clear cut. I realize there are implementation details to work out, but from a design perspective, what's missing here?
Implicitly allowing use of the nested type without specifying outer generic parameters seems like it'd have subtle library evolution implications.
// Module A
public struct Outer<Wrapped> {
public struct Inner {}
}
// Module B
func makesInner() -> Outer.Inner { ... }
If Module A now updates a method of Inner to reference Wrapped, only as an implementation detail, does Module B break? In the absence of e.g. a keyword to specify that Inner is generic-parameter-agnostic, the API contract is fragile.
This subtlety parallels how generated memberwise initializers are internal even on public types, as adding a new internal property could otherwise break clients.
I'm not sure what you mean by a "fully-qualified way"? Generic parameters can't be fully-qualified; they're just their name. (Which is presumably why in Swift 6, it's being disallowed for nested types to shadow outer generic outer parameters with their own generic parameters.)
Swift today already gives meaning to X.Y when X is a generic type; it implies type parameter inference:
struct X<T> {
struct Y {
init(_ t: T) {}
}
}
// Totally valid syntax that means the type of x is `X<Int>.Y`
let x: X.Y = X.Y(5)
If we allowed nested types that didn't capture outer context, the only way to know whether X<Int>.Y or X.Y was intended here would be to check every member of Y to see whether it references T, which as @Slava_Pestov mentioned above should be avoided, or to invent a new syntax to distinguish it.
How is that different than any other ABI break? If you're compiling for ABI stability, quite a lot of changes are off the table, including making previously non-generic types generic. In this case you've effectively changed the name of the type from Outer.Inner. to Outer<T>.Inner.
Sorry, fully specified? Not sure what the term is for changing X to X<String> in your example.
I went back and read Slava's recent replies and I'm not sure what you're referring to here. Doesn't the compiler already have to check to see whether T is referenced in the inner type in order to use it in the first place? From a user perspective, the example you give seems fine given Swift's already extensive type inference.
Consider this example:
// File1.swift
struct X<T> {
struct Y {
// members
}
}
// File2.swift
struct Z {
let x: X.Y
}
In a hypothetical world where nested types could exclude outer generics, this should be valid, but there's no context here to infer from. So, in order to type-check, the compiler when compiling File2.swift would have to do a deep traversal of all of the members and their bodies of Y in File1.swift in order to determine whether X.Y is valid (because Y doesn't use any captured context) or not (because it does). Today, typechecking X.Y only requires looking up to the declaration of Y.
Furthermore, consider another scenario: where File1.swift is instead a module's .swiftinterface and the bodies of Y are excluded. In that case, the compiler would already need to print an explicit keyword or attribute to indicate that the type is context-excluding.
Sorry, my use of "library evolution" put emphasis in the wrong place. My point here is about unintended API changes, not ABI compatibility. Given this code:
// Module A
public struct S {
private func f() {
<code here>
}
}
As far as I'm aware, in Swift today, nothing you can change within <code here> alone can cause clients who import Module A to begin failing to compile.
To clarify the previous example:
// Module A
public struct Outer<Wrapped> {
public struct Inner {
private func f() {
// Uncommenting the following breaks the spelling Outer.Inner?
// _ = Wrapped.self
}
}
}
Per Tony's description, uncommenting the line above would necessarily impact the .swiftinterface in your proposal. This is subtle and undesirable as a framework author.
Because of Swiftâs design for lazy type checking and incremental builds, any language feature where the type of a declaration depends on its body is pretty much off the table. (didSet accessors are an exception, but those tend to be short so itâs not much of a added cost, and yet it was still a pain to implement correctly.)
Also consider extensions:
struct G<T> { struct Nested {} }
extension G.Nested { func f() { ⌠use T ⌠} }
Is the nested type generic? What if the extension is in another file?
Yeah, that makes more sense, thanks for the explanation (and @allevato). Now that we've removed the ability to shadow generic placeholders in inner types, could we repurpose the syntax as an explicit acknowledgement you're using the placeholder in the inner type? I imagine such syntax could also be used to explicitly capture Self.
struct Outer<T> {
struct Inner<T> { }
}
protocol Outer {
struct Inner<Self> { }
}
Off the top of my head, I think that would at least cause problems if you wanted to reference that Self parameter in a where clause, because of how references in them are scoped:
protocol Outer {
struct Inner<Self> where Self: ... {}
^-- today, this Self resolves to Inner
EDIT: Oh no, I was wrong. The truth is scarier. The compiler actually allows you to use Self as a generic argument name when you're defining a non-protocol type, shadowing the built-in Self name:
protocol P {}
struct Inner<Self> where Self: P {
func f() -> Self { return self }
// error: cannot convert return expression of type 'Inner<Self>' to return type 'Self'
}
let x: Inner<Int> // error: type 'Int' does not conform to protocol 'P'
Should we... fix that?