Semi-explicit coercion control with `~`


#1

EDIT: This proposal is currently withdrawn, and I think a variation of RFC PR 248 is a better solution for the problems at hand. But ~ as a semi-explicit coercion operator can still be added later for user definable coercion rules, if they are implemented in Rust in the future.

Rust prefers to be explicit, but there are times when being too explicit can be ergonomic problems. Particularly, recently we are talking about introducing more coercions into the language:

RFC PR 225: Add safe integral auto-coercions. RFC PR 226: Allow implicit coercions for cross-borrows. RFC PR 241: Deref coercions.

Also discussions about introducing a Coerce trait.

The problems of implicit coercions is they may be too implicit. When we see a function call foo(bar, baz);, we will not be sure if the compiler is moving bar and baz, or taking their references, or launching nuclear missiles even before the body of foo gets executed.

This goes against Rust’s philosophy.

So, being too explicit and too implicit are both undesirable sometimes, then what to do?

@nrc’s idea about repurposing & to be a borrow operator got me thinking, that all we needed might be a sigil providing a middle ground.


Say we have a function fn foo(bar: &T), and a value fancy: Very<Deep<Chain<Of<Wrappers<T>>>>>, and we want to apply foo to the “meaningful” content of fancy.

We can do it explicitly, foo(&*****fancy), or implicitly with proposed implicit coercions: foo(fancy).

Both have their own problems. But usually the programmer doesn’t really care about exactly how many *'s he/she has to write, all he/she wants to know is that “some deferencing/coercions are happening and I may have to be cautious”.

So I propose we introduce a sigil for saying “some coercion magic happens here”, just like how ! in macros means “some code transformation magic happens here”.

And that sigil would be ~. The rule is:

  1. the language can define arbitrary coercion rules, even make them user-definable with a trait, but the only coercion rule that the language implicitly applies is auto dereferencing self in method calls;
  2. when a value is used in a position where possible coercions can be applied, the programmer must opt-in with ~, a prefix unary operator;
  3. it is a compile error to use successive ~s or use it when no coercion rules can apply.

So we can say foo(~fancy) then.

Why ~? It is unused and lightweight, and implies “slight transformations” in my eyes.

Why not ~? Because some keyboard layouts do not have a ~ key.

Personally I believe this is a good compromise between “no coercions” and “implicit coercions”.


Also, I think ~ can help with a problem introduced by auto dereferencing self in method calls:

If we have a bar: Rc<Foo>, when we call bar.clone(), which clone are we calling?

I propose that we use ~ as a “reverse coercion” operator in the suffix position here. The rules are:

  1. foo.bar() always calls the bar method on the “meaningful content”;
  2. if we want to call a bar method on some wrapper along the auto-deref chain, we use the syntax: foo~.bar();
  3. if there are multiple bars defined in the wrappers along the chain, foo~.bar() causes a compile error.
  4. Further disambiguation should be done with features like UFCS.

Also a good compromise in my eyes.


So, do you think these are good ideas or not? :wink:


#2

These are some ideas. I wouldn’t bother with it right now as it can be added after the 1.0 release. Because it is just an ergonomics feature it is better to wait how the typical rust code turns out after the adoption grows.


#3

Yes, I think this is a good idea. If macro syntax is changed from foo! to @foo and ! is not used fo error handling then ! could perhaps be used instead of ~.


#4

We used to have nearly all coercions be explicit using as. We moved to some implicit coercions (concrete type to trait object, for example) because it was hard work. I expect the pressure to be ergonomic will continue to outweigh the pressure to be explicit. It would also mean having two kinds of explicit coercion (as and ~). But perhaps having a custom coercion operator for user defined coercions is an interesting case.

Slightly tangential, I have an RFC coming up to precisely define the implicit and explicit coercions we have in Rust.


#5

I like the first suggestion – coercion – for ~, It’s novel, general and smart.


#6

@pepp, actually there may be some backwards incompatiable bits in my proposal, as there may be implicit coercion rules that I am not aware of.

The principle I’d like Rust to adopt is: if a value/variable/field is “bare”, i.e. not used with any unary operator or the method calling . (and ~. in my proposal), then no magic coercions of any kind should happen to it.

@Jexell, IMHO, ! is a bit too noisy, and it doesn’t have the “slight transformation” connotation, only “caution”.

@nrc, actually I mused with the idea of replacing as with a binary ~, but it might lead to people complaining “Rust is sigil soup” again. Or maybe not.


#7

I am warming up to the idea that a suffix ~ can be a better choice, because:

  1. It is in sync with the ?/! error handling proposal.
  2. ~foo may invoke incorrect expectations from C/C++ programmers, while foo~ will not.
  3. If we replace as with a binary ~, then a suffix unary ~ is more natural: the type part is simply omited.

fancy as &T -> fancy ~ &T -> fancy~.

Of course this will make the second part of my proposal ambiguous for programmers. But that part seems a bit arbitary anyway and can be dropped.


#8

Binary ~ is a bad idea it seems:

  1. Possible sigil soup.

  2. If we are using fully explicit coercions anyway, then saving one character with ~ seems pointless.

  3. Precedence problem:

According to RFC PR 204, suffix unary operators take precedence over prefix ones. Also, unary ops take precedence over binary ones, which means:

*foo~ would be interpreted as *(foo~), while *foo~T would be (*foo) ~ T.

Confusing.


#9

I realized that @nrc’s borrow operator proposal has two advantages over this if ~ is used as the borrow operator:

  1. ~foo and ~mut foo will always produce the same references no matter where they are used.

  2. It is a bit more explicit than this proposal, and the programmer can tell mutable borrows from immutable borrows. This is important.

Also, I think other forms of implicit coercions are not causing problems, so there is not much need to change them. If we later decide that we do need a semi explicit coercion operator (like, for user definable scoped coercion rules ala. Scala’s implicits), we can always add it then - postfix ~ won’t confilict with prefix ~.