Pitch: Fully qualified name syntax

True, that's something that would need to be taken into consideration. Although it's ugly, we could adopt a prefix scope resolution operator to indicate an absolute vs. relative lookup, so that e.g. ::Foo::Bar unambiguously refers to a module whereas Foo::Bar will pick up the innermost visible Foo.

5 Likes

To me prepending syntax doesn't feel natural in Swift. Is there anything in the language that's similar?

It would be great to have a more comprehensive model for call x's implementation of y. In most cases it can be accomplished with casting: (self as X).foo(), so my guess is it's a lower priority and a problem that won't be addressed soon.

It's also indicating something different enough that IMO it shouldn't use the same syntax. Some likely candidates should probably be considered so the two things wouldn't clash, but IMO it shouldn't prevent this from going forward.

I think that a decent argument could be made for reversing the meanings but I like the idea in any case.

It could be that we still want other mechanisms for addressing the other problems with protocols. Nonetheless, the name lookup problems that protocols introduce are pretty much exactly the same as those introduced by modules and extensions, so it seems to me like a qualified name syntax ought to be able to disambiguate both.

I would much prefer that the call site issue gets resolved with function call syntax
(ModuleA.A.doSomething(foo) or ModuleA.A(foo).doSomething())

One extra issue with both my suggestion and your ((ModuleB)B.default idea is that the extension could be defined in a different module than the protocol B. What's the fully qualified "name" of such an extension? How do we select a specific implementation if there are multiple competing extensions providing the same name, with differing constraints?

The parens syntax can accommodate all this, although it becomes quite unwieldy:

foo.(protocol B)doSomething()
foo.(extension B where Self: Hashable)doSomething()

Especially when we look at the fully qualified form:

foo.(ModuleB.protocol B)doSomething()
foo.(ModuleExtension.extension (ModuleB)B where Self: (Swift)Hashable)doSomething()

Embracing nested parens does improve consistency a bit here:

foo.((ModuleB)protocol B)doSomething()
foo.((ModuleExtension)extension (ModuleB)B where Self: (Swift)Hashable)doSomething()

Or, with @Lantua's approach:

foo.((ModuleB)B)doSomething()
foo.((ModuleB)B.(ModuleExtension)default where Self: (Swift)Hashable)doSomething() // ?

where clause is tricky, they don’t seem to be designed for inline usage. Declared module can probably be moved to the end with in keyword.

What about

(
Module.SubModule::Type.SubType
in DeclaredModule.SubModule
where Self: Module.SubModule::Type.SubType
)

So

foo.(ModuleB::B.default in ModuleExtension where Self: Swift::Hashable)doSomething()

And we don’t need full qualification all the time.

foo.(B.default in ModuleExtension)doSomething()
foo.(ModuleB::B.default where Self: Hashable)doSomething()
foo.(ModuleB::default)doSomething()
foo.(B.default)doSomething()

One thing to be vary of would be where clause in the middle, but the current system doesn’t allow it. So it should be fine. Should we really need it, we can replace where with enclosing []

What about using as to disambiguate?

(XCTest as import).XCTest

or

(XCTest as module).XCTest

It does left-out protocol qualification, since (foo as A) becomes immutable. We can probably lift the restriction, and incorporate where-clause to support constrained protocol extensions as well.

It still has this same problem though:

Not quite. Regardless of any particular languages rules for emphasis, people tend to remember things that come at the end of a sentence better than things in middle, but they also remember the beginnings of a sentence better. It's the stuff in the middle that tends to get lost, which is why languages usually put especially important information at one of the two ends of a sentence.

The Serial Position Effect is a fickle beast. (Curious if that plays into why C's thing.doAThing() style has ~won out over Smalltalk's `[thin doAThing] style). Anyway, good thing to keep in mind when designing APIs.

I agree with many others on this thread, and Xiaodi's rationale above in particular: it seems like :: for module qualification is the way to go.

I also generally agree with @Joe_Groff's comment:

My only concern with this is that it would be weird for ".foo" to mean "foo that is in some context specific scope" but for "::Foo" to mean "Foo is definitely not scoped". I'm not sure exactly how to square that circle :-)

One other random comment @beccadax - I found your explanation at the top of the thread to be extremely well written and easy to understand. Thank you!

-Chris

9 Likes

I think it's largely the same reason that Lisp never went mainstream. People don't like nesting brackets.

To be precise, Smalltalk doesn't use brackets for message sends; that's an Objective-C-ism. Smalltalk's brackets are blocks (anonymous functions).

3 Likes

Might be a little off-topic. Today we write A.B where type B is known to be nested in A in some way. Also when we write constraints we use T: A syntax. Wouldn't be useful to be able to have bidirectional type qualified types?

So if T is known to be a sub-type of A we could write T:A.B to explicitly refer to B over the super-type (A) from the base type (T)?

I had some ideas where it could make sense, but my brain discarded everything. :smiley:

Just for that possibility I'm in favor of the the pitched :: syntax because it does not collide with the mentioned idea.

We could theoretically reverse the lookup here as well: ClassName:SubModuleName:ModuleName


If that idea is non-sense from the view of an expert, please let me know so I discard it forever. ;)


EDIT:

If the above is non-sense in a way that it would be ambiguous for the compiler to parse and understand the difference between a constraint and a reversed lookup direction, why not allow 'constraint style qualified lookup'??

Instead of

let object: ModuleName::SubmoduleName::ClassName = ...

we could write:

let object: ClassName:SubmoduleName:ModuleName = ...

If I read the latter I would already understand that I want to store an object of class ClassName, which is from SubmoduleName which itself is from ModuleName.

How does this differ from T: B?

As discussed above, the standard in Swift follows the Parent->Child->Grandchild model. What would be the value of supporting the opposite?

let object: ClassName:SubmoduleName:ModuleName = ... is easy to reason about when they're named like that, but it will be confusing to parse if the hierarchy is opposite from the rest of the language. and, how do you spell a subtype of ClassName? I can't think of an order that makes sense.

let object: UnitTest:Utilities:Testable = ...
// then if there's a Type inside UnitTest I guess it would be...
let object: TestType.UnitTest:Utilities:Testable

Good naming solves part of the problem, but using left-right hierarchy when everywhere else in the language right-left is used will certainly be confusing.

1 Like

What if we pretty printed the Exprs here and fully qualified their identifiers too?

Slava

Some aspects of

ModuleName::SubModule::ClassName

worry me.

  1. Privileging modules. At some level, modules are just another name space. I suspect that there will be problems in the future for adding a new syntax that only works for them. Are they that special?

  2. The readability of a syntax that mixes operators:

Foo::Bar.Baz::Fred::George.Sam.Jane::Bill

  1. The long-term effect on module encapsulation. If my program is using A.B and suddenly a module I import defines a clash, how would I know? What would I do? Perhaps the unqualified B always refers to this module? (Source-breaking) Or is there some way to declare the implicit set of modules?

This proposal addresses a real problem. But I worry that next year there will be another such problem and another incremental proposal, so that in the long term, we end up with lots of different operators, etc. for the name lookup syntax. Is it possible to define something today which is neither too verbose, nor too obscure for a novice to read, and will extend and generalize to next year's problem?

2 Likes

I'm not sure we should subset out the member-lookup issue. It seems untenable for Swift to have two scope-clarifying syntaxes, and we clearly need one for member lookup at least as much as we do for global lookup. So the design of this should aim to solve both, or at least to clarify why different solutions are necessary.

3 Likes

I'd also prefer one lookup syntax that works with varying degree of specificity (module level, protocol level, protocol in module excluding submodule, etc.).

1 Like