[Pre-pitch] Allow extension of nested members within extension of parent

This is a tiny syntactic sugar pitch.

Swift's ability to group code by use of extension is probably one of the best features of Swift and something Apple themselves use a lot, combined with Swift's ability to nest types these two language features make up the backbone of what we love about Swift.

Today extensions are limited to global scope, I cannot extend a nested type within an extension of the enclosing parent type. This makes code hard to read, I would argue.

Simple case study (sorry for made up nonsense types and extensions...)

public struct A {
	public let one: One
	public let two: One
	public let notEncodableEither: NotEncodableEither

	public struct One { ... }
	public struct Two { ... }
}

Let's say we wanna extent all three types (of which two are nested), today we must write it like this:

// MARK: A Encodable
extension A: Encodable {
	public func encode(to encoder: Encoder) throws { 
		// manually encode `notEncodableEither`
	}
}

// MARK: A.One - Encodable
extension A.One: Encodable {
	public func encode(to encoder: Encoder) throws { 
		...
	}
}

// MARK: A.Two - Encodable
extension A.Two: Encodable {
	public func encode(to encoder: Encoder) throws { 
		...
	}
}

Nothing terribly wrong with this, but it is slightly less clear than, what I propose:

// MARK: Encodable (piched)
extension A: Encodable {

	public func encode(to encoder: Encoder) throws { 
		// manually encode `notEncodableEither`
	}

	of One: Encodable {
		public func encode(to encoder: Encoder) throws { 
			...
		}
	}

	of Two: Encodable {
		public func encode(to encoder: Encoder) throws { 
			...
		}
	}
}

The of label might be omitted if the community does not like it. But it reads out well: "extension of".

This syntax more clearly conveys what is being done here, we make A and some nested type conform to Encodable, which can now be done in the same extension.

What do you think?

1 Like

-1 from me simply because as you noted there's nothing terribly wrong with the original form.

What I would like to consider is this:

func A.foo() { print("hello") }
func A.One.bar() { print("world") }
struct A.One.Baz { var x = 42 }

maybe with a prefix (not sure if this is better or worse):

extension func A.foo() { print("hello") }
extension func A.One.bar() { print("world") }
extension struct A.One.Baz { var x = 42 }

Obviously this won't work with the protocol conformance specification as would be needed in your example.

Edit: Just note that putting protocol conformance methods in the corresponding extension is merely a coding convention, unchecked by the compiler, so this is totally valid:

extension A: Encodable {
    init(from decoder: Decoder) throws { ... } // OOPS
}

extension A: Decodable {}

extension A {
    func encode(to encoder: Encoder) throws { ... }
}

so could be this:

extension A: Codable {}

// MARK: A.Decodable
extension A.init(from decoder: Decoder) throws { ... }

// MARK: A.Encodable
extension func A.encode(to encoder: Encoder) throws { ... }
3 Likes

I don't agree, but that doesn't mean it shouldn't be an option. But why use of instead of extension?

2 Likes

This is actually more important than just syntax sugar. However, I do not support the use of the word “of” here, and I would prefer the natural spelling of simply “extension”.

Now, as to why it’s more than just sugar, suppose we have the following:

struct Outer {
  private struct Inner {}
}

The nested type Inner is visible inside the definition of Outer, and in any extensions of Outer within the same file. But we cannot currently extend Inner, anywhere. We cannot use dot-notation, because Inner is not visible at file scope:

extension Outer.Inner {}   // error: 'Inner' is inaccessible due to 'private' protection level

And we cannot use a nested extension, because those are not allowed:

extension Outer {
  extension Inner {}  // error: Declaration is only valid at file scope
}
12 Likes

I dont think this is very neat:

extension A { 
    extension One {}
}

For extensions with not that many LoC i just see ”extension … extension” which is less elegant than ”extension … of”

Unrelated REPL Question

I noticed that the following example works when you evaluate each "block" (struct/extension) in separate steps in REPL, but not when you evaluate the whole example together. As you suggested, it also does not compile (that surprised me!)

Can anyone explain to me why this can work in the REPL but can't be compiled?

struct Outer {
  private struct Inner {}
}

extension Outer.Inner {
  static func foo() { }
}

extension Outer {
  static func foo() { Inner.foo() }
}

Outer.foo()

And, by Hyrum’s law, we can be pretty confident that there are going to be people who actually rely on this behavior of nested private types as a feature.

Allowing nested extensions opens a whole can of worms with respect to access modifiers, because it’s currently baked into the design that extension is never nested. This is why private extension is a shorthand for fileprivate members—because private at the top level has the same effective visibility as fileprivate inside it. Proposals to change this have been rejected.

This is not to mention the other, likely more weighty unresolved questions that I’d expect nesting would introduce—for example, access to private properties of the outer type.

Which is to reinforce that this would not be “just sugar” but a major design and engineering effort, and whether it’s a good idea or not will be contingent on whether authors can first identify and second satisfactorily resolve the thornier issues that come up in a way that’s intuitive rather than adding difficult-to-reason-about complexity for the end user foremost and secondarily for the compiler implementation.

3 Likes

It just says of, though. You came up with it so it probably doesn't look weird and meaningless to you, but that doesn't necessarily translate to anyone else. :frowning:

Also, this problem is not contained only within extensions. It's a problem with all scopes. extension should be supported within all of them.

func outer() {
  enum Inner { }
  extension Inner { } // Declaration is only valid at file scope
}
2 Likes

Mmm, unless I'm misunderstanding, what your example implies in terms of what you think "should" be supported is beyond what @Sajjon is talking about, which is specifically extensions of nested types within extensions of the parent type.

If you're saying that, anywhere a type can be declared, an extension of some type should be allowed as long as the type is visible (and thus utterable) in that scope, the implication is that one should be able to extend, say, Swift.String at function scope. Let me just point out that this absolutely should not be permissible.

When a type T defined in library A is extended by library B, what you really have is a type "T-as-extended-in-B." There are already a number of issues which arise when, say, an end user imports library B and a library C that both extend T in conflicting ways. However, one invariant which we get "for free" by requiring extensions at top level only is that there's at most one T-as-extended-in-Foo for each library Foo, because extensions themselves cannot be scoped (this is why an access modifier in front of extension is regarded a shorthand for an access modifier for each of its members, rather than behaving like an access modifier in front of a type declaration). This invariant must be preserved even if extensions are allowed to be nested.

If an extension can be scoped, as you imply, then there would be infinitely many T-as-extended types allowed in the same module, and indeed it would be possible for func f to return a value of type T which is extended differently than type T visible in func g to which the value is next passed as an argument. This would be extraordinarily messy to reason about even if the compiler could support it.

Only adding conformance would be a problem. The compiler already has an error message that could be updated for usage in a local scope:

…' modifier cannot be used with extensions that declare protocol conformances

While I wouldn't use it, because I don't like the additional nesting, adding conformance in an internal nested scope should be permissible, as should adding conformance to a locally-private type. But this is a separate feature than extension being nestable in general—the original post here conflates the two.

If this compiles…

protocol Protocol { }

private struct Struct { }
extension Struct: Protocol { }
extension Struct {
  var `protocol`: some Protocol { self }
}

var `protocol`: some Protocol { Struct().protocol }

…this should compile.

protocol Protocol { }

var `protocol`: some Protocol {
  struct Struct { }
  extension Struct: Protocol { }
  extension Struct {
    var `protocol`: some Protocol { self }
  }
  return Struct().protocol
}

I'd argue that this behaviour is very confusing, as is public extension.

I can't quite see how someone could rely on the unextensibility of Inner, when done inside Outer, since only people with write access to the original file could do this. Maybe I misunderstood Nevin, but I thought the idea was that the extension had to be written in the same extension as Inner was defined? Either way, that could be a requirement.

Slightly easier example to make Jessy's point more clear:

If this compiles:

protocol Proto {}

struct A {
    struct B {}
}

extension A.B: Proto { /* ... */ }

this should this:

protocol Proto {}

struct A {
    struct B {}
    extension B: Proto { /* ... */ }
}

be it with "Proto" conformance or without.

2 Likes

Ah yes:

struct Foo {
  private var hasInvariants: Bar
  
  private struct Bar {
    // Invariants may be temporarily broken here, but nowhere else.
  }
}