Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Open
Skeltpr opened this issue Jul 26, 2022 · 21 comments
Open

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

Skeltpr opened this issue Jul 26, 2022 · 21 comments
Labels
leads question A question for the leads team long term Issues expected to take over 90 days to resolve.

Comments

@Skeltpr
Copy link

Skeltpr commented Jul 26, 2022

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:

  • Rust: has a multitude of de-refering and ref-ing embeded into the language, but always uses . for access.
  • Go: Also opted to use . exclusively while retaining a very similar syntax for derefering and pointer types.
  • D, Nim, Swift also just treat references like normal variables with . operator.
  • C#: A language, that while doesn't fall into the same category as the rest of these and Carbon, I've still seen referred to as C++ true successor by a lot of higher-level language users. It, too, exclusively uses . when dealing with it's reference features (out, in, ref).

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
Copy link

jwtowner commented Jul 26, 2022

Linking these related issues and discussion.

#520
#523
#1454
#1583
#1545

@OlaFosheimGrostad
Copy link

OlaFosheimGrostad commented Jul 26, 2022

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
Copy link

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
Copy link

sor commented Jul 26, 2022

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
Copy link

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;

  • (C++)
if (my_smart_ptr)
    my_smart_ptr->do_stuff();
  • (Carbon)
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();
}

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

@sor
Copy link

sor commented Jul 26, 2022

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
Copy link

WarEagle451 commented Jul 26, 2022

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
Copy link

sor commented Jul 26, 2022

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
Copy link
Author

Skeltpr commented Jul 26, 2022

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
Copy link
Author

Skeltpr commented Jul 26, 2022

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
Copy link

sor commented Jul 26, 2022

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

@chandlerc
Copy link
Contributor

chandlerc commented Aug 10, 2022

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.

@jonmeow jonmeow added the leads question A question for the leads team label Aug 10, 2022
@chriselrod

This comment was marked as resolved.

@geoffromer

This comment was marked as resolved.

@chandlerc

This comment was marked as resolved.

@nigeltao
Copy link

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
Copy link
Contributor

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
Copy link

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
Copy link
Contributor

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

This comment was marked as outdated.

@github-actions github-actions bot added the inactive Issues and PRs which have been inactive for at least 90 days. label Nov 24, 2022
@jonmeow jonmeow added long term Issues expected to take over 90 days to resolve. and removed inactive Issues and PRs which have been inactive for at least 90 days. labels Nov 28, 2022
@jonmeow
Copy link
Contributor

jonmeow commented Nov 28, 2022

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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
leads question A question for the leads team long term Issues expected to take over 90 days to resolve.
Projects
None yet
Development

No branches or pull requests

10 participants