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
.
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
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).
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.
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.
What if we pretty printed the Exprs here and fully qualified their identifiers too?
Slava
Some aspects of
ModuleName::SubModule::ClassName
worry me.
-
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?
-
The readability of a syntax that mixes operators:
Foo::Bar.Baz::Fred::George.Sam.Jane::Bill
- 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 unqualifiedB
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?
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.
I'd also prefer one lookup syntax that works with varying degree of specificity (module level, protocol level, protocol in module excluding submodule, etc.).