[SUGGESTION/RFC] Replace scope resolution operator '::' with member access operator '.'
Hi,
The idea behind that change is to change/simplify the syntax of how we access namespaces components. If the notion of namespace is shifted a little bit to an instantiated "static sparse struct", then the scope resolution operator '::' can simply be replaced with member access operator '.'.
PROS:
- It unifies the syntax when accessing static/constexpr variables.
- It simplify the teaching of the navigation in the symbol namespace by eliminating the special case of the scope operator.
- It eases moving code from namespace variables to member variables. As such it should also be an opportunity to reduce diffs in scm.
- It should also simplify tools like code generators, by unifying the syntax we don't need a special case for generating namespace member access.
CONS:
- There might be some corner cases when trying to access types, but I'm quite convicted the ease of write surpass the burden of theses corner cases.
- I wonder how error prone the unary left '.' operator can be when accessing the global scope.
For now I don't consider it a complete suggestion, but I wonder what other people think about that idea, hence the RFC.
I also initially wanted to replace :: with ., but as I started writing cpp2 more I really enjoyed the distinction when using UFCS.
Simple example:
a: std::array = (3, 1, 2);
a.std::ranges::sort();
This is in the initial roadmap (bottom of the ReadMe):
I was discussing with a friend, and we both agreed that the visual distinction between an static scope access and a member access might be important in the language. To which extent, we don't know, it could be counterbalanced by making generic code easier to write and maintain.
In any case, I don't think this is implementable today in cppfront, which makes experimentation on it hard 😦
I really like the idea of this unification. I'm been a big fan of the idea of closing the gap between classes and namespaces, and this unification would definitely be stepping stone in that direction. Maybe that's also because I've never been a big fan of the :: operator in C++, which is quite hard to teach/explain. Also I recently watched this talk which confirmed my gut feeling that the current scope resolution and member binding model in C++ could be improved.
However, I expect the interaction with UFCS would to be somewhat quirky. Indeed, @zaucy's example highlights the fact that operator precedence between :: and . is essential. If we were to use only ., how is the compiler supposed to figure out that the correct evaluation order is:
a.std.ranges.sort()
^ ^ ^
#3 #1 #2
The compiler error would likely be something like "no member named std in std::array<...>", which could be quite confusing. The solution here would be to introduce braces, but I don't think this is very readable:
a.(std.ranges.sort)()
This makes me think of an idea that I had for a different language. To have a custom symbol syntax to allow any kind of symbols. In the idea it would be something like : a.$"foo.bar(baz)"(42) This is a made up syntax to get the idea of how it would look like. It would allow to "ease" bindings to other languages with different function signatures (though it would not help with calling conventions).
For now I don't see a solution with UFCS, other maybe than what Lua do (if I remember well), which is also not particularly nice.
At a second thought, do we really want a fully qualified "path" with UFCS?
For me, having a fully qualified path defeats a little the purpose of the syntax, and (I guess) most real world usage could be covered by a local "using namespace". That last idea, is what I think would be the most used, within a template context, switching from local members to UFCS, to implicitly provide default objects behavior.
As I currently understand the feature, the primary goal of UFCS is to ease generic programming by providing a generic way to call functions which may be member functions or free functions depending on the context.
From that perspective, being able to call explicitly std::ranges::sort (or std.ranges.sort in the new proposed syntax) should not constrain too much the design here, since one can always either use the traditional free function style std.ranges.sort(a) when the std function should always be used, or make an unqualified call when the right implementation should be picked via ADL + overload resolution. For the latter, adding a using statement (using std.ranges.sort; a.sort();) feels like idiomatic C++ to me, as this is already something we do with the copy-and-swap idiom for example.
Regarding my previous error message concern, I guess we could add a "braces might be needed here if UFCS is used here" or "consider introducing a function alias to disambiguate the call" when multiple .s appear in a failing function call. This would provide a decent diagnostic.
If this is the only corner case to address, it is worth giving it a shot IMHO. However, in the context of a transpiler, we would need to be able to decide whether each . must be transformed into a :: or left as is, and I don't know how feasible this is.
I'm been a big fan of the idea of closing the gap between classes and namespaces, and this unification would definitely be stepping stone in that direction.
I have to point out that this doesn't bring classes and namespaces closer: Member selection within a class is also done with :: (in C++, not sure if cppfront has deviated from that). The distinction with object.member is not class vs namespace, it's compile time object vs runtime object. :: currently looks up members in a dictionary in the compiler's memory, while . means the member is to be calculated relative to a runtime offset (or a function call has to be passed the left hand as a parameter).
I'm all for unifying the compile time and runtime syntaxes. Just saying that that's the context where this suggestion should be evaluated.
I wonder about getting the reference of functions/methods.
Currently methods and static functions cannot have the same name (can be another suggestion/RFC). If that restriction is removed at some point, would that syntax still interact nicely?
I think so, but I would like other optinions.
Does that mean that method pointers would be like void (Foo.*bar)(baz)?
(I did not look if syntax changed for that, or it was discussed) Looking at
it as this, it looks like a cons.
Another possible cons, it makes the named aggregate initialization ambiguous. I don't know if changing the syntax for it is wise though.
What if you had both :: and . but treat :: as a higher priority dot. e.g.
a.b.c.d == a.(b.(c.d))
a.b::c.d == a.(b.c).d
This may be my personal taste, but there are at least 2 "programming war crimes" that I see comming with fully qualified path in UFCS:
foo.::bar::baz() That one is half decent foo.(typename ::bar::baz<A, B>)::bat() this one really hurts my eyes
So I really think the "using" idium is the way to go.