Pitch: Fully qualified name syntax

That’s certainly unreadable as I have been looking at this for a few minutes and have not worked out what the expression would mean.

1 Like

Well, in reality we have naming and capitalization conventions, so this would generally be something more like:

coneLaser.BlasterKit::recentBlasts.Swift::forEach { blast in ... }

Modules are special in relevant ways:

  1. Modules exist in an exclusive, flat namespace which contains only modules, not other declarations. It's possible that this will change if we add a submodule feature, but not in a way that eliminates the module-only namespace at the root.
  2. Every non-module declaration* belongs to a module, so modules are cross-cutting. There are other things that cross-cut in this way, like declaration kinds, but they aren't things that users can create.
  3. The only thing you can do with a module is look up declarations inside it. You can't, say, print a module or constrain a generic parameter to a module.
  4. All of the declarations in a module are written and compiled together, so you can generally assume that there is someone who can control all of the code in a single module and resolve any conflicts, but there may be no one person who controls all of the code involved in a conflict between different modules.

Basically:

  • You can't really use modules in most normal ways, so we don't really lose anything by giving them a special syntax.
  • They occupy a unique position that makes them especially well suited to resolving ambiguity caused by lack of coordination between different teams, so if modules are given a special syntax, there's little reason to believe we would want to extend it to allow non-module criteria.

I totally agree that this isn't ideal, but (a) you don't need to qualify any name unless there's an actual conflict, (b) when there is an actual conflict, there's no getting around the need to write this much information, and (c) the language doesn't currently allow you to declare submodules so as things stand today, you'll never need to write something like Baz::Fred:: anyway.

Perhaps another syntax would visually distinguish the module names more, but there's only so much a syntax can do. And once module names are syntactically distinguishable from other names in source code (because they only appear in an import or before a ::), perhaps syntax coloring can assist with readability.

Generally, you'll know because you'll get a compile-time diagnostic like 'B' is ambiguous for type lookup in this context. Currently, you can usually fix this for top-level types by explicitly specifying A.B; with this change, you'll always be able to fix it for types at any level by specifying A::B (and possibly naming the parent types).

There are some situations where behavior can change silently—for instance, the new module extends a type to add a method which overload resolution considers to be a closer match than the one it chose without that module—but that kind of ambiguity is sort of the nature of a feature like overloading. And once you do notice the problem, you can again resolve it by explicitly specifying A::b(arg), even in situations where the language does not currently offer a solution.


* Actually, I think some of the operator precedence declarations might exist outside of any module, but you can't address these in normal code anyway.

6 Likes

My understanding is that we are currently splatting the user's source code (with minor redactions of things like #if) into the bodies of inlinable functions rather than trying to convert the AST back to source code, because a type-checked AST may contain information that can't be represented in the surface language.

As a qualification tool, Modules are't that much more special than declarations (class, struct, enum, protocol, extension). You can nest types to create namespace, and some already uses empty enums in place of submodules.

Still, whatever we end up doing, I suspect we'll need to dropdown at least twice; Module/Submodule -> Declarations, and Declarations -> member. Especially when Modules can have the same name as Declarations.

1 Like

Right, but since the type checked AST was constructed from parsed AST, it should be possible in theory to print something back out again. Whether this is a practical undertaking, I don't really know.

1 Like

Whatever we go with, it would be very nice to be able to easily reach outward to the current module scope for type names, syntactically. As it is, these shadowing situations below are tied to the name of the app/module in a way they should not be, and you can't even use the patterns in playgrounds. This results in bad naming being common because shadowing is painful.
(I'm looking at you, CodingKeys. :eyes:)

struct Thing<T> { }

struct Type {
  // Without "AppName." module scoping, this error occurs:
  // Type alias 'Thing' references itself
  typealias Thing = AppName.Thing<Int>
}
protocol StuffDoer {
  func doStuff()
}

// Without "AppName." module scoping, this error occurs:
// Type 'StuffDoer' constrained to non-protocol, non-class type 'StuffDoer'
func ƒ<StuffDoer: AppName.StuffDoer>(stuffDoer: StuffDoer) {
  stuffDoer.doStuff()
}
1 Like

I also agree on using :: as the scope resolution operator. It would be great if this could also apply to operators from different modules so I get to pick which module’s operator I use.

:: is familiar to C++ and Rust developers, and it works reasonably well there. However, extensions make Swift's naming more complicated than what's covered by :: in those languages. The fact that every component of a fully qualified name may include a separate module path makes :: an inappropriate choice in my view.

E.g., consider John's example:

coneLaser.BlasterKit::recentBlasts.Swift::forEach { blast in ... }

Capitalization does not seem to help me at all -- I can't help but parse this as . having stronger precedence than ::, resulting in nonsense:

coneLaser.BlasterKit :: recentBlasts.Swift :: forEach { blast in ... }
┕━━━━━━━━━━━━━━━━━━┙    ┕━━━━━━━━━━━━━━━━┙    ┕━━━━━┙

(Also, module and type names are both capitalized by convention, so capitalization won't help at all in cases like ModuleA::TypeA.ModuleB::TypeB.)

Well-designed syntax highlighting can mitigate this by dimming the module names and the corresponding delimiters:


However, I feel strongly that Swift's syntax should remain human readable even in contexts where syntax highlighting is not available.

There may be no way to make such expressions look nice -- the interjected module names will always stand out. But we should at least try to make them reasonably easy to understand.

The reason I pushed for the parens + juxtaposition idea is that juxtaposition is pretty much the only thing that "obviously" binds tighter than the dot. Having some sort of start/end delimiter seems unavoidable, but we don't necessary need to choose simple round parens. Here is how it looks with strawman guillemets:

coneLaser.«BlasterKit»recentBlasts.«Swift»forEach { blast in ... }

Unlike ::, this syntax naturally guides me to read «BlasterKit»recentBlasts as a single unit. Syntax highlighting helps here, too, but unlike ::, highlighting isn't essential for understanding:

The delimiters also allow a measure of extensibility that's missing from Foo.Bar::Baz. We can put things other than module names between the delimiters to e.g. identify the particular protocol/extension whose member we wish to mention.

If only we could figure out what ASCII strings to use in place of « & »...

22 Likes

One way to resolve the confusion while also keeping :: is to require parenthesis around all module qualifiers:

  coneLaser.(BlasterKit::)recentBlasts.(Swift::)forEach { blast in ... }

Taking this a step further, we could move the names themselves into the parens:

  coneLaser.(BlasterKit::recentBlasts).(Swift::forEach) { blast in ... }

Since :: reads well at the beginning of an expression, we could allow the parens to be omitted in that specific case:

  SpaceKit::coneLaser.(BlasterKit::recentBlasts).(Swift::forEach) { blast in ... }

This would also keep the syntax open for extensions, perhaps allowing us to disambiguate between calls to methods defined in competing extensions:

  SpaceKit::coneLaser
    .(BlasterKit::recentBlasts)
    .(Swift::forEach in extension Sequence) { blast in 
      ... 
    }
7 Likes

We somehow land on this strange design space largely because . is well chosen (oddly enough) as lightweight identifier separator. If we want to add something of higher precedence in operator style (<identifier><sigil><identifier>), we'd need something visually lighter for it to work well, which :: is not.

So I agree that bracketing them would be better, it's the only thing I can think of that binds tighter than . and allows for future extension.

Best be it stayed that way. I use vim on virtually daily basis, and highlighting there is... not ideal at times.

2 Likes

Keep in mind that weirdness only effects disambiguating calls to specific implementations. Types don't have modules, so there would never be that weirdness with an import or a Type/function reference.

// Disambiguating Module/Type
import Module::Foo.Bar // ?? You're importing a Type, not a module.

let thing: Module::Foo.Bar // ✅ Bar is a Type inside Foo inside Module.
let thing: Module::Foo.Bar::SubModule // ?? how can a Type have a Module?

With this it's not possible to intermingle sigils. :: always comes before and not after .


//  vs disambiguating specific implementations
myArray.Swift::Array.map {}
myArray.Module::Foo.map()

In this case one can intermingle to their hearts content.

I'm unsure the same sigil should be used for both these things. Libraries are imported constantly and Types/libraries often have namespace collisions. Calling specific implementations has a much smaller use case and IMO should be discouraged in most cases as a poor design choice.

It would be a shame to over-complicate the syntax for 99% of use cases to support a 1% use case.

1 Like

It's a good point, but that's already hard to say what 99% use cases actually are when the usage is already in the minority.

(seems not to be required, but a nice to have) :: also doesn't work well for full-qualification when you may need to include extension constraint to specify extension.

One thing I'm still not clear on is the scope of this pitch.
Do we include only module qualification? What about protocol and extension qualification? Should it only work on inline environment, or should it also work with declaration?

OP seems to be concerned mostly with module qualification, but it could be easy to paint ourself into the corner if we're not careful.

We could extend the current `backtick` syntax for this purpose. Any backtick-quoted name with a dot could become a fully-qualified name:

  `SpaceKit.coneLaser`.`BlasterKit.recentBlasts`.`Swift.forEach` { blast in ... }
  `SpaceKit.coneLaser`
    .`BlasterKit.recentBlasts`
    .`Swift.forEach in extension Sequence` { blast in 
      ... 
    }

Unlike parens, there's no way this will be confusing with function calls.

(Currently, Swift won't accept dots, colons, spaces, etc in backticks, so this ought to break nothing.)

7 Likes
// Module A
struct Foo {}

// Module B
struct Foo {}

// Module C
import A
extension Foo {
  struct Bar {}
}

// Module D
import A
extension Foo {
  struct Bar {}
}

// App
import A
import B
import C
import D
let x: (A.Foo).(C.Bar)
3 Likes

Based on what I've seen in Swift and otherwise, it's pretty rare calling specific implementations is needed, but you got me wondering if Swift's protocol oriented nature may change that compared to the other languages I use, so I may reconsider my stance :)

I'm pretty sure that would just be let x: C::Foo.Bar

This is a much better identifier than a preceding ModuleName:: as it doesn’t get lost visually.

However, I like michelf’s much better as it fully envelops the “thing” so it’s chunked into the same things visually as it is philosophically. I feel it also benefits from already being a Swift concept and this feel like a natural expansion of its current meaning.

Okay, then I leave it for you to extend the example so that what I wrote is necessary.

My point is I don't think it ever is...