[Pitch] Module selectors

Alright, folks, it's time to take another crack at this.

There are a bunch of situations where one name shadowing another can make it impossible to address the declaration you want. One common example that comes up in module interfaces happens when a type has the same name as the module it belongs to:

// In module XCTest:
open class XCTest: NSObject { ... }
open class XCTestCase: XCTest { ... }

// In module MyTestHelpers:
import XCTest
extension XCTestCase {
    public func myAssert(...) { ... }
}

// In generated MyTestHelpers.swiftinterface:
import XCTest
extension XCTest.XCTestCase {    // Error--this looks for `XCTestCase` in `XCTest.XCTest`!
    public func myAssert(...) { ... }
}

But there are a lot of minor variations on this theme that can come up in both module interfaces and human-written code.

To fix this, I propose adding a syntax called a "module selector" which always, unambiguously finds a top-level declaration in a given module. That way, the module interface can say something like this and get the right behavior:

import XCTest
extension XCTest::XCTestCase {    // `XCTest::` is always interpreted as a module
    public func myAssert(...) { ... }
}

I also propose supporting a module selector on member lookups:

_ = foo.Bar::baz()    // calls the `baz()` declared in `Bar`, not one from an unrelated module

The full draft proposal is available here. This idea has previously been discussed in a pitch from 2019.

58 Likes

I'm not totally convinced of the spelling, but this is a much needed feature, and I'd rather have it under a suboptimal name than not have it at all, so I very much want this to move forward no matter how it's spelled.

19 Likes

The logic behind the current choice is discussed in the last section of the proposal, but I'm open to alternatives.

2 Likes

I think the proposed spelling makes sense given the similarities with C++ namespaces and Rust modules.

2 Likes

Yes please, let's do it. https://youtu.be/V6QhAZckY8w?feature=shared :)

1 Like

Would it also be reasonable to use this spelling for protocol selectors, for a type that conforms to multiple protocols with same-named requirements and/or extension methods?

9 Likes

Love the feature, love every single future direction listed!

I had an idea on how to unambiguously solve the protocol referencing issue mentioned in the future directions:

The super expression could gain the ability to be parametrized by the name of the type whose slice of the instance is being referred to. Specifying a protocol name will provide access to symbols defined in an extension on that protocol (enabling disambiguation between conflicting defaults from different protocols). For a class, or a class-bound protocol, it can also be any superclass in the inheritance chain. The only circumstance where the super parameter would be optional would be in a class, which would default to the direct superclass, retaining the current behavior.

protocol P1 { }
extension P1 { var name: String { "P1" } }
protocol P2 { }
extension P2 { var name: String { "P2" } }
class C1 { var name: String { "C1" } }
class C2: C1 { override var name: String { "C2" } }

class Example: C2, P1, P2 {
    func describe() {
        print(super<Self::P1>).name) // Can use module selector
        print(super<P2>.name)
        print(super<C1>.name)
        print(super<C2>.name) // Same as `print(super.name)`
    }
}

+1 Please, and thank you. We've sorely needed this for years.

I also fully support the future direction of using _:: as shorthand for the current module. If only so one can still do module based look up in situations where one doesn't know what the module name is (Compiler Explorer and playgrounds mostly).

Now if we just had a lighter way of making namespaces than uninhabited enums we'd be all set.

3 Likes

Yes please. Let’s finally tackle this problem. I can’t count how many times I had to advise people not to declare top level types with the same name as the module.

5 Likes

I have a future direction discussing this. The tl;dr is that I don't think we can because allowing a protocol name on the left side of :: means we'd have to do an unqualified lookup to resolve that identifier, which reintroduces the shadowing and ambiguity we're trying to avoid with this feature. Probably best to use something that's syntactically distinguishable to handle protocols.

6 Likes

Additionally, I think the problem inherent with the example given in your link above isn't that you want to disambiguate between two requirements with the same name at the call site. It's earlier that that—any type that conforms to both of those protocols may only implement a single witness that simultaneously satisfies both very semantically different requirements. We need to formalize something like @_implements instead.

1 Like

One way to do that could be to allow the disambiguator to appear as part of the declaration name, if that were the direction we're taking:

protocol Weapon { func draw() }

protocol Shape { func draw() }

protocol Star: Shape, Weapon {
  func Weapon::draw() { ... }

  func Shape::draw() { ... }
}
7 Likes

…but we could do that just as well with “normal” Weapon.draw syntax in that position.

1 Like

Perhaps. Would value.Weapon.draw work in a method invocation though?

1 Like

(I don't want to rathole on this too much since it's not strictly part of this proposal, but) My gut feeling is that a concrete type that implements protocols with colliding requirements should be forced to rename at least one of the witnesses to make concrete call sites completely unambiguous, with the special syntax only used in the definition to connect the witness to the requirement so that it can be resolved for generic and existential dispatch. A concrete type that always requires you to write something like value.Weapon::draw in order to use it feels broken from an API design point of view.

This is almost what @_implements does today, except that it also allows the requirement's original name to be used, so it can't actually be used to solve collisions:

protocol P { func f() }
protocol Q { func f() }
struct S: P, Q {
    func f() {}

    @_implements(P, f())
    func g() {}
}

S().f()  // still ambiguous because S.g is a candidate
6 Likes

This doesn't help with a protocol composition—there's no way to say which draw() you want to call on an any Weapon & Shape. Same for a generic parameter <T> where T: Weapon, T: Shape.

6 Likes

Oof, right.

For the any Weapon & Shape case, I would question whether value::Weapon.draw is better than just (value as any Weapon).draw, but there's not a good equivalent to that for the generic constraint case that doesn't require decaying to an existential.

2 Likes

Ahh… good news! It sounds like this would also work for freestanding macros?

Yes, macro expansion expressions are covered:

macro-expansion-expression → # module-selector? identifier generic-argument-clause? function-call-argument-clause? trailing-closures?

4 Likes

Ahh… good news!

And would we expect something like this is what a freestanding macro would look like:

import MyMacro

let a = 17
let b = 25

let (result, code) = #MyMacro::stringify(a + b)

And an attached macro:

import MyMacro

@MyMacro::Macro class C { }