Kind of beside the point but it would be nice if as some P worked for this purpose.
+1. I donât like :: too much but the prior art is ample to support it. Itâs just an unfortunate set of symbols.
This is one of those features that can be hard to explain but is needed.
// In module IonThruster
extension Spacecraft {
public struct Engine { ... }
}
// In module RocketEngine
extension Spacecraft {
public struct Engine { ... }
}
// In module NASA
import IonThruster
import RocketEngine
func makeIonThruster() -> Spacecraft.IonThruster::Engine { ... }
In Spacecraftthre are two possible Engine structs, I want the one defined in the IonThruster module. ![]()
But so needed! So I am grateful for this proposal.
That could also give us a place to hang a module selector for choosing one conformance out of several:
// Use the `Encodable` conformance from `IllBehavedModule` even if there are others:
print(try JSONEncoder.encode(obj as IllBehavedModule::some Encodable))
But I think this is a big enough feature without inventing as some casts too!
+1 I like this!
I think the "visual weight" issue of :: can be fixed with syntax highlighting (not sure what the procedure for introducing a highlight change in a syntax proposal would be tho). Just highlight the Module:: with the same color as whatever comes after it, making it look like one thing
Examples:
Cannon::makeBall()would be highlighted as ifCannon::makeBallwas the name of the functionCannon::Ballwould be highlighted as ifCannon::Ballwas the name of a type
We sort of already have this behaviour for functions whose names are wrapped in backticks, so it isn't completely out of the blue.
Yes please! Sorely needed, particularly for macro expansions and modules with public types that share their name.
Iâm quite happy with ::, it has precedent in C++ and Rust, and this is niche syntax.
The proposed grammar doesnât cover nested modules, e.g. Apple has stuff like Darwin.POSIX::pthread_t & I think the stdlib modules like _Concurrency nest like Swift._Concurrency::Task.
I suspect thereâs a case where the list of scopes passed to macro expansions to show their lexical context should be âfully qualifiedâ in this way⌠perhaps:
import A // provides X
extension X {
@YourMacro
struct Whatever {}
struct X {} // insane but legal?
}
Then the context will currently include extension X but if the macro generates X itâll refer to the sibling type thatâs invisible to the macro? Whereas if the context provided extension A::X then the macro could do the right thing?
Finally!
Great to see this picked up, the number of times I/we had to steer packages away from reusing the module name between a module and a type has been really big and we ended up with silly solutions like suffixing "...Module" to module names to not conflict with the ... (like ServiceContextModule containing ServiceContext or HeapModule containing the Heap type, just to name a few
).
The :: spelling sounds good to me, and I like the other ideas where qualifying can come into play maybe...
For reference, in case you'd like to mention in alternatives considered: the Scala solution to this is pretty ugly it is a special identifier _root_. Since everything in Scala is in "some package" (like Java) the top level package is _root_ and even if something shadows bar: object bar you can still get to the package bar by doing package bar { ... }; object bar { _root_.bar.function() }
But just using the idea of Module:: is much cleaner I think ![]()
Swift currently only supports submodules for clang modules, and it only really treats them as distinctive in the import declarationâduing name lookup, the imported submodules all get flattened into the top-level module. So with this change you would write Darwin::pthread_t, just as you can currently write Darwin.pthread_t.
(This is discussed very briefly in âParsing detailsâ.)
Instead of an entirely new syntax, can the compiler emit a diagnostic when it sees conflicting types, and require an explicit full module path to disambiguate in those cases?
What would that explicit full module path syntax be? The proposal is introducing a new syntax because isn't a way to write a module-qualified declaration name today without the potential for this ambiguity.
I briefly had the same thought but no. That is because modules can extend types in other modules by adding exactly named members.
//declared in EnginesKit
Spacecraft {
func start()
func explode()
func coolDown()
}
// In module IonThruster
extension Spacecraft {
public struct Engine { ... }
}
// In module RocketEngine
extension Spacecraft {
public struct Engine { ... }
}
// In module NASA
import IonThruster
import RocketEngine
func makeIonThruster() -> Spacecraft.IonThruster::Engine { ... }
Let's say you import both IonThruster and RocketEngine
Because Spacecraft is declared in EnginesKit, the true full path Spacecraft is EngineKit.Spacecraft, not IonThruster.Spacecraft or RocketEngine.Spacecraft
You could think "Well couldn't IonThruster and RocketEngine re-export the struct (or typealias it)", kinda? but then it could only export a version of the class that only contains the members from the parent module EnginesKit and the members on its extension. This would introduce the concept of a "view of a type".
With out introducing a "view of a type system" an extension allows you to extend type (not a view of it) , so when you import the other module, those re-exported (or even typealiased) version of the struct would ALSO get the extended members so you've solved nothing.
You would have to have very complex system to decide what extensions expose or extend which 'views' of the struct.
I think this defeats the purpose of extensions. Because in most cases you do not need to qualify a type with its module.
This explanation is kinda convoluted, but I hope I did it justice.
Big fan of this proposal overall, but I do have concerns about the interpretation of :: in qualified lookups, as seen in the Spacecraft.IonThruster::Engine example. This reads to me as searching for the Engine symbol in the Spacecraft.IonThruster package. This isnât just a matter of associativity, since there is no top-level Engine in the IonThruster package either. So what happens if one is added?
// In module IonThruster
struct Engine { ... }
extension Spacecraft {
struct Engine {
// not the same as the top level type
}
}
// In module NASA
import IonThruster
func makeIonThruster() -> Spacecraft.IonThruster::Engine { ... }
// which Engine does this refer to?
Even if the rule as pitched can successfully disambiguate this situation, it seems potentially very confusing that IonThruster::Engine refers to the top-level type in one context but a nested type in a different context. It also unfortunately prejudices the language against ever supporting native Swift submodules.
I think it would be more consistent to put the module before the whole type: IonThruster::Spacecraft.Engine. The immediate challenge is that IonThruster might export its own top-level Spacecraft type, in addition to extending the one it imported from elsewhere. I believe this can be solved with nesting: IonThruster::(Spacecraft::Spacecraft).Engine would refer to the Engine type nested within the IonThruster moduleâs extension of the Spacecraft type imported from the Spacecraft module.
In this example from the proposal:
// Module RocketEngine
public struct RocketEngine { ... }
public struct Fuel { ... }
// Another module
import RocketEngine
_ = RocketEngine.Fuel() // Oops, this is looking for a nested type in the
// struct RocketEngine.RocketEngine!
This seems like the compiler should be able traverse types both with the leading RocketEngine as either a module name or top-level type, and then diagnose a call as ambiguous if there are conflicting ones.
And if there's a conflict:
// Module RocketEngine
public struct RocketEngine {
public struct Fuel { ... }
}
// Another module
import RocketEngine
struct RocketEngine {
struct Fuel { ... }
}
If there were conflicting ones, the compiler would require us to use either:
_ = Self.RocketEngine.Fuel()
or
_ = RocketEngine.RocketEngine.Fuel()
where Self in a top-level context refers to the module (assuming code in a module shouldn't know its own name).
Perhaps there are edge cases with extensions? But somthing like this.
What you're proposing wouldn't parse today, though. It would require implementation of the rejected alternative Add a fallback lookup rule for module name shadowing. I think that alternative is rejected for a few good reasons:
- It's not foolproof because you can always construct a scenario where it wouldn't sufficiently disambiguate. What if
struct RocketEnginecontains its own nestedRocketEnginetype? There would be once again no syntax you could write that would unambiguously refer to the top-level declaration. - It doesn't provide a way to solve the problem of unambiguously referring to extension members.
I think this is solved by the compiler requiring Self and full module path if there are potential conflicts visible to the module:
// refers to nested type in current module
_ = Self.RocketEngine.RocketEngine.Fuel()
// refers to type in module RocketEngine
_ = RocketEngine.RocketEngine.Fuel()
This isn't just a problem for code written in the same module, though. What if you need to refer to the top-level RocketEngine type from code in a different module?
Self also seems problematic as the syntax for "this module", since you might be writing code in the context of a type that Self could also refer to, introducing a new opportunity for ambiguity.
I think I agree with this feedback. Not merely because of the associativity angle, but because I think it reduces to a simpler mental model where IonThruster:: indicates that we are referring to whatever is that module's "view" of what Spacecraft.Engine is. At the very least, I think that spelling should certainly be permitted even when Spacecraft is only extended but not declared in IonThruster.
By contrast, it takes some mental backtracking to comprehend Spacecraft.IonThruster::Engine, since you're asking the user first to conceive of what this scope understands as Spacecraft, figure out what type that corresponds to at the top level of IonThruster, and then determine whatever that module considers to be the Engine nested therein. Sure, this is a level of complexity that is certainly explainable, but in this specific example it is certainly not necessary and, in my view, not preferable.
And yes, it is true that @ksluder's alternative would be defeated if IonThruster both simultaneously vends a public extension to Spacecraft::Spacecraft that adds a nested type and simultaneously vends a distinct public type IonThruster::Spacecraft which might also have a nested public type of the same name. But, besides there being a plausible but longwinded way to disambiguate, I think it's also fair to characterize that scenario as Swift not forbidding an obviously bad API design choice that doesn't need to be accommodated specifically in our design considerations here.
These issues can arise not due to the design choices (good or bad) of any one author, but due to the independent choices of two authors whose libraries happen to be used by the same client. I think it would be unfortunate if we did all the design work here and still ended up with edge cases where you can't unambiguously refer to a declaration without breaking it into multiple references. It doesn't strike me as totally implausible that I want to pick S.R where S comes from module A (with a conflicting definition in module B) and R comes from module C (with a conflicting definition in module D). Specifying one module up front doesn't resolve thatâwith extensions in the mix, "which declaration are you referring to" is fundamentally a property of the reference path component, not the path as a whole.
I think itâs still possible to resolve the pathological case @xwu mooted:
// module Spacecraft
struct Spacecraft { }
// module IonThruster
import Spacecraft
struct Engine { }
struct Spacecraft {
struct Engine { }
}
extension Spacecraft::Spacecraft {
struct Engine { }
}
// module NASA
import Spacecraft
import IonThruster
let _: Engine // ambiguous use of 'Engine'
let _: Spacecraft.Engine // ambiguous use of 'Spacecraft'
let _: Spacecraft::Spacecraft.Engine // 'struct Engine' in 'struct Spacecraft' in module 'Spacecraft'
let _: IonThruster::Engine // 'struct Engine' in module 'IonThruster'
let _: IonThruster::Spacecraft.Engine // 'struct Engine' in 'struct Spacecraft' in module 'IonThruster'
let _: IonThruster::(Spacecraft::Spacecraft).Engine // 'struct Engine' in extension in module 'IonThruster' of struct 'Spacecraft' from module 'Spacecraft'
Since any given S.R must be nested in some particular S at the point of its declaration, allowing :: to mean the LHS module's top-level "view" of what S is (and not only the module in which S is originally declared), I am fairly certain it would always suffice to refer to C::S.R or D::S.R absent within-module shenanigans I describe above that arise from the deliberate choice of a single vendor.
Put concretely, I cannot conceive of a motivating issue where the full generality of qualifying each reference path component is actually required in order to mitigate ambiguities arising from combinatorial use of libraries that are not deliberately trying to step over each other. And this is the reason why I think @ksluder is right that hoisting module qualification is the way to go.
Fairly contrived, but consider:
// A
struct U {}
typealias S = U
// B
struct S {}
// C
// file1.swift
import A
extension U {
struct R {}
}
// file2.swift
import B
extension S {
struct R {}
}
// MyModule
import A
import B
import C
C::S.R // (B::S).(C::R) or (A::U).(C::R)?
This is arguably still 'within-module shenanigans' but I find it a little tough to say that the author of C has done something wrong here, especially considering that the typealias in module A might have been introduced in an update supplied after C defined the extensions to A::U and A::S.