carbon-language / carbon-lang

Carbon Language's main repository: documents, design, implementation, and related tools. (NOTE: Carbon Language is experimental; see README)
http://docs.carbon-lang.dev/
Other
32.28k stars 1.48k forks source link

Please consider changing `->` to `.` for pointer access #1725

Open Skeltpr opened 2 years ago

Skeltpr commented 2 years ago

Long story short, -> is one of the most miserable operators to type out. I have tried desperately to use alternatives to C++ simply on the basis of not wanting to type this operator thousands of times. As the C++ successor, I'm really hoping Carbon can at least address this problem early-on prior to becoming a major standard that would break all code if changed in the future.

While this may seem arbitrary, as I haven't seen this addressed anywhere else in Carbo discussions so far, it does have heavy precedent in modern systems-level languages. Especially Rust, which Carbon appears to be heavily inspired by:

The point I'm trying to make is that -> is pretty archaic. It would be one thing if Carbon treated pointers "unsafely" like C (I like to think of -> as a C/C++ safety measure to remind yourself you're working with a nullable type), but with Carbon's lack of pointer arithmetic and pointer nullability, its pointers are much more like modern references from the languages described above.

In fact, Carbon pointers are more like C++ references, which... yup, also opted to use . operator. Hell, aren't C++ "references vs pointers" not the perfect example of C++'s horrible obsession with retaining compatibility as described in Chandler Carruth's talk? Carbon's pointers take all the brilliance of C++'s references, EXCEPT for the syntax part. Why should Carbon inherit such a weird "core" syntax when all modern languages, including C++ from over two decades ago, have provided a better alternative?


Now there would be a drawback to removing -> completely from the language because it would hurt interoperability with C++. While uncommon, there are C++ classes that require using overloaded ->. But, that has nothing to do with how pointer syntax in Carbon should work. You could keep the -> operator as an overload-able option, but make it so the . operator on pointers default to pointer access on pointers.

Hell, if you had a pointer to a class instance that overloaded ->, you could use -> on that pointer to run that overload, creating a very consistent standard in the language where . ALWAYS accesses the fields and -> is ALWAYS for special circumstances. I know I'm not the only one who's worked with C++ iterators or third-party code and had to sit back and think about whether I want . or ->.


I'm sure an argument could be made that replacing . with -> should be just something I configure in an IDE, and god knows it's one of my favorite reasons to use Qt Creator, but there's still places where I want to refactor code and change a type from pointer to value or vise versa, and it just becomes hell to replace the line of property assignments I do in a constructor/throughout the file. (Or times when the IDE just straight-up lags and I just end up typing arrows explicitly anyway). I would argue with Carbon pointers being so much similar to C++ references and Carbon values, this type of refactoring will be significantly more frequent.

I'm crazily in love with Carbon, and I love how so much of the design borrows from modern systems-level languages. So I hope it can become inspired just a little more to make pointers just a bit nicer. Thanks for reading.

jwtowner commented 2 years ago

Linking these related issues and discussion.

https://github.com/carbon-language/carbon-lang/issues/520 https://github.com/carbon-language/carbon-lang/issues/523 https://github.com/carbon-language/carbon-lang/issues/1454 https://github.com/carbon-language/carbon-lang/issues/1583 https://github.com/carbon-language/carbon-lang/discussions/1545

OlaFosheimGrostad commented 2 years ago

Both Rust and D allow name bindings in the smart pointer object to shadow names in the pointed-to-object.

Please don't replicate this mistake.

chriselrod commented 2 years ago

Long story short, -> is one of the most miserable operators to type out.

This probably varies a lot by keyboard layout, but perhaps you could configure yours? Or perhaps do so on the editor level? I know this is a little off topic, but I just mean to say that ergonomics like this can have solutions from multiple angles.

sor commented 2 years ago

For my UK layout the key presses compare press "." with: press minus, hold shift, press ".", release shift I guess regardless of layout a "->" will always be at least two presses (unless you make your own custom one, I once had "this->" bound to the caps lock button 😄)

WarEagle451 commented 2 years ago

While so far I haven't dived deep into Carbon's pointer system my main concern with this suggestion would be ODR. If Optional is implemented in a similar way to C++'s smart pointers, having -> is actually a life saver. As someone who mainly uses C++ -> is the cleanest way to separate the methods of a smart pointer from the methods of the pointed member. This problem could easily be avoided by doing the following;

But if you ask me the C++ way is just better, no need to worry about definition collisions.

sor commented 2 years ago

If we need to distinguish, then we need some syntax to do this, but this would only be a rare case. I rather have a more complicated syntax for the 1:10000 case, than having a more complicated common case. The common case should be simple and intuitive.

var a : *T;
a.empty(); // no ambiguity what this means

var s : smartPtr<T>; // T has member function empty()

// maybe
s.empty();  // could operate on the smart ptr BUT: people might expect the wrong thing to happen here
s->empty(); // could operate on the pointee

// or
s.smartPtr::empty();
s.T::empty();
// sure, this is more ugly than just . or ->, but it is very explicit. The prefix (T::) could be omitted
// if there is no ambiguity, but must be written if there is a collision.
// Should be quite simple to produce errors that help you in case of a collision

// the syntax could also require braces, to make longer expressions easier parseable?
s.(smartPtr::empty)();
s.(T::empty)();

This would only apply to methods which collide.

I would rather write a bit more verbose and explicit code in some edge cases, than sacrifice general ease of reading/writing code. To me the proposed pointers in Carbon sound more like references that can be rebound, which I find a good thing.

I work on a code base that uses intrusive pointers and therefore all types which want to be smart'pointed to, would already need to comply to certain rules imposed by the smart pointer, so we would not have any collision at all.

WarEagle451 commented 2 years ago

s.smartPtr::empty();

vs

s->empty();

Using '::' would create confusion for anyone who uses C++, but this is Carbon so we shouldn't care. But what we do care about is interloopability as mentioned in the Cpp North talk, using similar grammar makes that goal more achievable.

sor commented 2 years ago

10000 times: s.empty(); 1 time: something more complicated This language already does not look like C++, which is a win in many regards

Skeltpr commented 2 years ago

Thank you for all the discussion. Just to clarify, as stated in the original post, I do not see any need to remove -> from the language entirely. This is specifically about allowing the primitive pointer type to use .. Hell, I don't see why -> and . can't both be valid FOR primitive pointers.

Unless Carbon provides the ability to overload ., which has not been confirmed afaik and not what this issue is requesting, smart pointers will continue to use ->, which I'm fine with. I don't have to use smart pointers, or I can create my own. But primitive pointers? Those are fundamental, and they are used just as frequently (or in many cases more frequently) than value types.

(With that said, above is just my best case compromise in hopes of allowing this feature to exist. Personally I agree with @sor, and if I were in complete control of Carbon, I would purge -> from all Carbon-exclusive code and leave it only for C++ interop. There are other ways you can provide smart pointer methods, as demonstrated by literally every other modern programming language, and the one in a thousand case should not dictate how you handle the other 99.9% of your code).

Skeltpr commented 2 years ago

If Optional is implemented in a similar way to C++'s smart pointers, having -> is actually a life saver.

I have to wonder if it actually will be? Part of the brilliance of Rust's optional is you're forced to "extract" the value by using match or if let, therefore forcing your code to null-check. I would imagine Carbon would expect you to do the same with how similar it is to Rust already.

if (MyOptionalPtr.Empty()) { // I'm not 100% sure if this code would actually compile, it's like this to express my point
    MyOptionalPtr.Member().Empty();
}

With that said, even if Carbon's optionals did work like this, Carbon's ImplicitAs feature would allow for implicitly converting from Optional(MySmartPointer(T)) to Bool. Something like this?

// not 100% this is valid code either, but it also demonstrates my point
external impl Optional(MySmartPointer(T)) as ImplicitAs(Bool) {
  fn Convert[me: Self]() -> Bool {
    // check if optional valid and smart pointer valid simultaneously
  }
}

// ---

if(myOptionalSmartPtr)
  // myOptionalSmartPtr now ensured not "None" and not "null"
sor commented 2 years ago

Please also have a look at this related proposal from @OlaFosheimGrostad : #1736

chandlerc commented 2 years ago

I think this is an interesting and important question, but I want to call out one aspect of how the discussion is happening just so we can keep things focused and productive:

The point I'm trying to make is that -> is pretty archaic.

I think it will help to find ways to talk about the specific advantages and drawbacks of the options here. I don't think calling a syntax "archaic" helps much -- it seems hard to draw any specific conclusions, and also seems likely to not be the most friendly description for folks actively using (and maybe enjoying!) C++ and C even today.


Anyways, focusing on the specific proposal, this is essentially the same model suggested in other contexts as "implicit dereferencing" of pointers. I do think it is a reasonable model to consider.

There is a major drawback of the model that I don't think you're really addressing: it makes it very difficult to differentially refer to methods on the pointer vs. the pointee. In C++, this only really impacts smart pointers as normal pointers have no methods.

However, in Carbon we want APIs to be provided by the library which means we will want even raw pointers to have methods. I think an important simplification of the language with pointers vs. references is that it is very unambiguous when referring to the pointer vs. the pointee, and it seems likely to be even more impactful in Carbon.

Modeling this as implicit dereferencing leaves open some options for referring to the pointer distinctly from the pointee, but the models tend to be fairly complex, especially in a generic context. Does it apply only when using .? Why not when passing as an argument? Let's consider the current plan for handling infix binary operators where a + b is translated to a.(Add.Op)(b) -- would this only implicitly dereference a, but not b? That seems likely to be very surprising. But automatically dereferencing parameters also carries its own surprises.

It also may hide the depth of dereference from the reader. For example, x.Foo() when x is a 3-level pointer may have a very surprising performance cost. That possibility is more effectively telegraphed to the reader (IMO) with the syntax (**x)->Foo().


Note that there is an alternative strategy to arrive at . exclusively being used, and that is to change how dereference occurs so that it composes cleanly with .. The most obvious way to handle this is with a postfix operator so that x*.Foo() would be the equivalent of x->Foo(). This was one of the many factor discussed when resolving #523 and recently re-raised in #1454. Currently this is felt to be too large of a deviation from familiar C++ expression syntax at this time.

chriselrod commented 2 years ago

However, in Carbon we want APIs to be provided by the library

That link is to this issue; did you mean to link elsewhere?

geoffromer commented 2 years ago

However, in Carbon we want APIs to be provided by the library

That link is to this issue; did you mean to link elsewhere?

I'm guessing he meant to link here

chandlerc commented 2 years ago

However, in Carbon we want APIs to be provided by the library

That link is to this issue; did you mean to link elsewhere?

I'm guessing he meant to link here

Yes, doh. I left the link empty to fill in later and then forgot. I've updated my comment. =]

nigeltao commented 2 years ago

Putting an idea (not necessarily a good one) out there... we could possibly have our cake (distinguish between -> and . operators, especially with smart pointers) and eat it too (enjoy the ergonomics of typing o.x regardless of whether o is pointery):

The official Carbon spellings of C++'s p->x and v.x are p.->x and v..x (or whatever bikeshed color you like). These are ugly, but will almost never be seen. Insted, as syntactic sugar, o.x automatically chooses between o.->x or o..x whenever it's unambiguous (based on the type of o).

geoffromer commented 2 years ago

The official Carbon spellings of C++'s p->x and v.x are p.->x and v..x (or whatever bikeshed color you like). These are ugly, but will almost never be seen. Insted, as syntactic sugar, o.x automatically chooses between o.->x or o..x whenever it's unambiguous (based on the type of o).

This does still have the problem that o.x might initially be unambiguous, but then become ambiguous when a member is added to the type of o or *o. That means adding a member to any pointer type or any pointee type would be potentially a breaking change.

nigeltao commented 2 years ago

True, although that's not entirely a new problem: adding methods to a base type can already break subtypes. Carbon culture will also presumably Live at Head and endorse Large Scale Changes.

geoffromer commented 2 years ago

True, although that's not entirely a new problem: adding methods to a base type can already break subtypes.

Yes, but you can avoid that problem by disallowing inheritance from your type. This problem would unavoidably affect all types.

Carbon culture will also presumably Live at Head and endorse Large Scale Changes.

Yes, but that doesn't mean that breaking charges will have zero cost, it just means they won't have infinite cost.

github-actions[bot] commented 1 year ago

We triage inactive PRs and issues in order to make it easier to find active work. If this issue should remain active or becomes active again, please comment or remove the inactive label. The long term label can also be added for issues which are expected to take time. This issue is labeled inactive because the last activity was over 90 days ago.

jonmeow commented 1 year ago

Marking this long term since I'm not sure leads will have a quick answer here.