I'm not sure I understand the point about merely adding dependencies.
We're stipulating that library C vends a type X.Y.Z for public consumption—that (including whichX and whichY) is part of its hopefully considered public interface. The motivating ambiguity we're trying to address with this feature is that which arises when the client imports various combinations of libraries that may extend X.Y in clashing ways. We address this head-on once we can express something like "give me X.Y.Z from the perspective of C."
It is not that the author of C can't ever write a source-breaking future version of the library that swaps out one X.Y for another. But that's of a kind with other, obviously ill-considered API breakages (for example: what if Foundation swapped out Date with a type that semantically doesn't represent a point in time at all?), and feels orthogonal to the problem we're trying to solve.
Since this pitch is strictly about resolving ambiguities by naming a module, I’m actually not convinced it’s the right feature to solve the member lookup issue at all. Case in point, your C++ example shows disambiguation by type: b.B::foo().
This is almost certainly how Swift programmers will also want to disambiguate between members. The module prefix is still useful for unambiguously naming the type for that disambiguation, and the strawman Swift equivalent to the C++ example I propose would be something like (b as ModuleB::B).foo(). Or perhaps b.(ModuleB::B.foo)() to avoid taking a dependency on a new as cast feature, but that drags in the whole question of how to spell unapplied functions that are overloaded by keyword arguments.
I think that’s the pathological case that is resolved by nesting. But I take your point to be that the original pitch is source-stable if the ambiguity is added in a future revision of X or Y. Is that accurate?
Yeah, a change in any of the modules (A, B, or C) could make the references to X or Y no longer source-stable, whereas qualifying a step directly with the module that provides it should always be stable.
Hmm, can you give an example where you think an ambiguity from the perspective of C would get propagated to its clients in a way that's unresolvable by the author of C? I'm afraid I don't understand the source stability issue you are referring to.
Perhaps I naively assumed we had consensus that this is a pathological case. That is, I perceive it as affirmatively the responsibility of library C to vend some stable notion of what C::X.Y.Z is regardless of what module owns X or Y (or, if it cannot, choose not to nest its type like this or choose not to make it public).
With some form of this feature, it will become possible deliberately to create X.(B1::Y).Z and X.(B2::Y).Z in the same module file. While we don't need to (and probably shouldn't) go out of our way to ban this, I think it is acceptable—good, even—for Swift to be opinionated that making both available as part of the public interface of a single module is a setup for users to have a bad time. And if our design for module selectors instead encourages the author of C to name these types X.B1YZ and X.B2YZ without ambiguous nesting, then I see that as a plus and not a minus.
Well, we have (just assume everything is public because I can't be bothered):
// module A
struct X {}
// module B
import A
extension A {
struct Y {}
}
// module C
import A
import B
extension A.B {
struct Z {}
}
No reasonable source evolution could remove any of those struct declarations, so the fully elaborated type A::X.B::Y.C::Z is perfectly source stable no matter what module we write it in. C::X.Y.Z means that the Z must be the definition provided by C, but I don't see any reason why the resolution of X and Y must be unambiguous "from the perspective of C". Suppose we add a fourth module:
// module APrime
struct X {}
C could add this as a public dependency. C presumably needs to decorate its uses of X with A::X now, but that doesn't propagate to its clients because importing C doesn't implicitly transitively import its public dependencies, so clients don't automatically see APrime::X. However, it does presumably affect this novel concept of "from the perspective of C", and now we need additional language features in order for modules to (as you put it) satisfy their affirmative responsibility of making C::X.Y.Z a stable concept for clients. To me, this seems like a very surprising new thing to thrust on library authors; they'd just have to anticipate it, I suppose.
Yeah, I've been sloppy in describing what I am counterproposing here for the semantics of ::, which clearly should not leak non-public details that clients wouldn't otherwise see. Certainly, if you stand in the shoes of C and consider what's visible in a top-level file in that module, there's both APrime::X and A::X. I agree with you that I don't like that "perspective."
Consider instead a client of C with the following top-level code in a standalone file:
import C
typealias XYZ = X.Y.Z
Whatever XYZ resolves to here, I think that should be the meaning of C::X.Y.Z in a file that imports all manner of libraries. (This doesn't account for the module-name-is-type-name issue which of course :: should disambiguate, but modulo that issue I think this rule-of-thumb captures what I'm thinking of as "the perspective of C.")
It's my (separable) opinion that libraries should vend types that don't require clients to use module qualification for disambiguation so long as the client doesn't have another import that creates the ambiguity. But what I would propose for :: is that if there is no ambiguity in the above reference to X.Y.Z, so too should there be no ambiguity in C::X.Y.Z. That is to say, while I wouldn't necessarily ban qualifying every component (to support the macro use case), if you don't need to write import A in order to utter the typealias given above, you shouldn't need to write C::(A::X).Y.Z in another file either if, say, you also import APrime.
It seems to me that to the extent we can (and I think should) expect human users to be able to use a single library without defensively qualifying everything, this is exactly the same extent of source stability but now extended to files importing more than one library.
d.swift:3:9: error: cannot find 'X' in scope
1 | import C
2 |
3 | let z = X.Y.Z()
| `- error: cannot find 'X' in scope
So the naive rule just doesn't work. You can try to fix it by also importing other things besides C itself, but then you have to decide what to import, and I don't know what that would be besides "all of its public dependencies", and then you've inherently made a module's public dependencies part of its API, which seems like a really bad idea.
Ugh, I'd forgotten that nested types aren't one of the magical scenarios (like operator declarations) where we bend the rules of what's in scope.
Yeah, "all of its public dependencies" would be out of the question.
It could be, I think, what's actually imported at the point of use. So you either must write one of C::(A::X).Y.Z or (A::X).Y.(C::Z) or else you'll need to be in a file that's imported A in order to write C::X.Y.Z.
Effectively, it transforms :: into having the semantics of an expression-local import with higher precedence than file-local import.
I do. But not simply as a stylistic point because of the visual heaviness of ::.
Rather, it stands to reason that having to disambiguate one component is going to be more common in human-written code than having to disambiguate several. Not only would I (as would most, I reckon) prefer not requiring parens in the common case, but I’m trying to devise rules that allow you to hoist the disambiguation so you don’t have to be so fastidious about which component of a nested series or which module of an import hierarchy unless and where it truly matters.
It falls out that making . bind more tightly than :: better suits that desired result.
I broadly support this proposal. I found the spacecraft.ionthruster::Engine confusing at first but I see why it works this way given the constraints.
It’s my understanding that this syntax is purely used to disambiguate module membership, and not a generic scope resolution operator. As such, I would like to see current module lookup get resolved as part of this proposal and not deferred to a subsequent one. One reason is that since the current module name is not fixed but can differ between targets, this is a significant gap right now.
My preferred syntax would be ::TypeName, where not specifying a module name before the operator resolves to the current module.
I also have some clarifying questions:
Is a nested type considered to be a “member” of the outer type, or is it merely part of the outer types scope?
Will the current ambiguous syntax ModuleName.TypeName (e.g. Swift.Array) continue to work?
I'm thrilled to see this proposal. As one of the maintainers of XCTest, I have personally experienced the pain and inconvenience of having to troubleshoot issues stemming from this problem—both in XCTest itself and its clients (transitively up the dependency graph). I've also adopted the various, often fragile workarounds for it, and helped client projects implement them too. Solving this will be extremely helpful.
Beyond those benefits, I welcome the new expressivity of module selectors on member lookups. That's something we've needed in Swift Testing several times recently.
The ability to qualify members by module is necessary to disambiguate when two extensions from different modules have declared the same member on the same type, but I suspect this situation is much rarer than the case where a type has inherited conflicting members from its conformances or its superclasses. It’s also not sufficient if the conflicts come from the same module.
I would be really sad if the syntax for module-based member disambiguation made it difficult or impossible to later support submodules when type-based disambiguation might require a different syntax anyway.
Is it possible to extend this to have a default syntax for saying "the current module"? There are a couple places where this would be useful:
Playgrounds, which don't have a name for the top level module
Modules intended to "swap in" to an old version of a module. I often have a "MyModuleNew" WIP module which when I'm done I need to find-replace any module references within it. It would be great if this just worked when I renamed the module to "MyModule".
A #module macro which resolves to the current module could be used in function declarations as a default argument and refer to the caller‘s module (similar to #file or #isolation).
Instead of a special operator we could just spell it out with a compiler command like this:
_ = foo.#module(Bar).baz()
If this is something people will rarely see, spelling it out like that makes it easier to understand that we're injecting a module name here.
This would also fit with the current module qualified name syntax: Swift.Array simply becomes a shortcut for #module(Swift).Array when there is no ambiguity about Swift being the name of a module. Same dot syntax as usual, just being explicit that we're looking for the module and not something else with the name Swift.
Edit: and #module by itself (no parens) could refer to the current module.
I think that if we ever implement a form of submodules that's relevant to module selectors*, we can use TopLevel::Sub::foo() to write them. This is currently a parse error, so adding it won't be source-breaking as long as TopLevel::foo() continues to look into the contents of clang submodules.
* For instance, if submodules function like @_spi—filtering out declarations from being imported, but not introducing a separate namespace—then they can't introduce conflicts that can only be resolved by a submodule selector.