Propose new operators to reserve / include for edition 2018

@Uther I think these methods can be moved into respective Traits without a breaking change. Just guessing, though. I don’t know whether such a refactoring has been done before.

To keep allowing the use of for example wraping_add without having to use a new trait we would have to include the trait in the prelude. That would be a breaking change because people can already have a trait in scope with the same name. We would have to include a lot of new traits this way.

To be honest i dont think a.checked_add(b)? is common enough to have it’s own syntax. Maybe a.checked_add(b) without applying ? automatically would be better however then +? would not really fit.

1 Like

Not necessarily. If use some_mod::CheckedAdd overrides the prelude import of CheckedAdd, then it should be fine. Besides, we can introduce a v2 prelude in the new edition if we like.

Or (as is necessary for backwards compatibility anyway) keep the inherent u*::checked_* methods even when a trait for the operator is added. We already have duplication of methods between trait implementations and inherent implementations.

1 Like

I’ll throw in another vote for Deref and Index being adjusted so that they can return any sort of proxy, not just a ref proxy.

Instead of type Output = T and also -> &Self::Output, could we just have -> Self::Output directly and then have type Output = &T when appropriate and type Output = ReflikeTProxy at other times. This would greatly expand the sorts of data that could work with the “builtin” ops.

They do return references for a good reason, though. If the return value is not a reference, then either…

  1. you couldn’t write &container[idx], or
  2. &container[idx] wouldn’t have type &T, or
  3. indexing would (occasionally) desugar into two method calls, as in .index(...).deref(), instead of just one,

neither of which is a good thing.

1 Like

Forgive my rust-fu ignorance, but if the output type is re-specified as a ref type, and then the output of the operation is the output type directly, then it’d end up allowing the same thing as currently happens with existing cases. The difference is that you’d also be able to specify new proxy types as well.

What I want is to be able to write, for example,

// slices a rectangle from position 2,2 up to (but not including) 10,8
my_bitmap[(2,2)..(10,8)]

And then you get a thing back that is a 2d slice out of the 2d whole. It’s trivial to do, absolutely trivial, but you just can’t do it with the current definitions of the std::ops types. Don’t just take my word for it, ndarray has a whole alternate slicing system that doesn’t get to use the normal ops, because it’s impossible to make the types line up. I’m not a rust master, but I really believe that there has to be a way to fix that.

5 Likes

Yes, iff the return type is &T.

@Lokathor Edition upgrades need to always work for warning free code. The adjustments to Deref you propose cannot be implemented in Rust 2015 because they constitute a breaking change. Nevertheless it’s highly likely that something could be done to make your use case work. You should open a new thread for that. Unless, of course, you want to propose a new operator for this.

1 Like

I agree a.checked_add(b)? is, currently, not common enough to deserve a custom syntax. That's why I only suggested to reserve it for later.

Because if, in the future, we can get ranged types that prevent arithmetic operations that can overflow, at compilation time, ( as suggested here ), it may become really useful.

Such is not the case. Where would the lifetime of the & come from? What is the output type of deref_mut? (not that it matters, since as @MajorBreakfast said, epochs must not change libs)

Dag, that's way conservative. Even when you're opting into a new epoch it can't have any changes that break anything? I thought we landed in a slightly more flexible spot, but that was a very long discussion.

Anyway, proxy objects sure doesn't work for the existing ops definition (there already was a thread). Basically str and slice work because of built-in language magic for linear fat pointers.

If you wanted to make some new operator that was slightly funny looking like val[#proxy_key] as a new way to "index and get a proxy" I guess that'd be slightly useful. The main benefit of having it work with normal Deref is the deref magic method resolution. If you can't get that, there's a lot less point to, well, any of it. You're going to have to implement a ton of code in two or three places (or more!), so you're pretty hosed. If would be useful if "DerefProxy" also got the deref method lookup magic, but I imagine that it might explode the compilation complexity to do that :stuck_out_tongue:

One of the best arguments against operator proliferation is the existence of “use English;” in Perl. If having lots of operators was a such a great thing, then, why did “use English;” exist? Have you ever tried to work on a large Perl application that didn’t “use English;” and “use Strict;”? It sucks.

2 Likes

Operators seem scary some of the time, and you can make things very hard to understand with operators, but you also can make your code very hard to understand without operators. That’s a code quality factor, not the fault of operators. Your case of use English; seems (from a quick google search) to be more about giving names to variables which are normally named by just some punctuation mark. You can already do that with operators in Rust right now, if you want to, but people usually don’t want to, so they don’t. In fact this very topic is all about how people wish that they could make the code flow on the page more like it flows in their head.

Compare with Haskell, where you can make up any operator at any time, and what you get is people using all the “core” operators (things like $, <$>, <*>, >>, >>=, ++, and so on) all of the time, and then sometimes they’ll use weirder operators (!?, //, ^+^, ^. etc) that are usually specific to a particular library, and your comfort with them is basically based on if you’ve seen the library before.

Which, when you think about it, is the exact same as with any other type of code. If there’s some random blob of rust you’re presented with, it doesn’t matter if there’s methods or operators, your ability to understand what’s going on has the most to do with if you’re familiar with the crates that the rust code is using or not.

1 Like

IIRC this would effectively require GADTs, a relatively advanced and powerful type-level feature. As far as I know there’s not even a proper RFC for GADTs though, so building on top of that is out of the question for now.

It's not really about scary so much as readable.

I would not tend to agree with that beyond the trivial idea that you can just use meaningless method names like "dog" to mean "cross product". I would tend to always find the following:

let a = b.cross( c )

more readable and more easily recognized than some combination of punctuation characters as an operator:

let a = b ~*~ c

for example.

That is why, rather than focusing on introducing more punctuation-style operators into the language I would prefer that we introduce an in-fix function calling convention. For example, if you have a method "foo" that takes two arguments, you can call it like this:

let a = BType::foo( b, c );

or you can also currently call it like this:

let a = b.foo(c);

I would like to also allow (something like):

let a = b ~foo~ c

or possibly

let a = b :foo: c

So instead of having the methods of special Operator Traits map to punctuation (aka operators), just be able to call the method in an in-fix style. Then if you had, for example, cadd (checked_add), csub (checked subtract), cmul (checked multiply), and cdiv (checked divide) methods on a type that did add/sub/mul/divide with checks for overflow on a type, you could write expressions like:

let a = d ~cmul~ ( b ~cadd~ c )  ~cdiv~ e

Instead of:

let a = d.cmul( b.cadd( c ) ).cdiv( e )

Now, for this example, because it is add, subtract, multiply, and divide, you could make the argument that some punctuation would be more appropriate, but, once you get away from basic operations like this, unless you are going to allow unicode characters as operators in the source, the random streaming together of the few available punctuation characters to try to create "operators" seems counter-productive (to me) when you could just allow an in-fix style method call syntax and then you can create meaningful and clear names for all your interesting operations. Really, at the end of the day, that is all operators are - alternative names for an arity-2 method called using in-fix syntax. Why not just allow the method name instead? Why have garbled punctuation trying to map to a bunch of different concepts?

Maybe (and here I think is a BIG MAYBE) if you allowed operators to be defined using Unicode operator characters it might actually be worth it, but, that presents a whole bag of problems with editors, IDE's, etc. that makes it feel like the juice isn't worth the squeeze.

If Rust did decide to allow for definition of lots of different operators, I would think the approach should be:

  • figure out how to allow unicode characters as operators and understand how that will be supported by the editor/ide ecosystem
  • create Traits for all the Mathematical or Operator-Like characters in Unicode (NOT things like Emojis, Wing-Nuts, Bat-Shits, or random Klingon characters) with clear explanation and documentation on the trait as to the expected behavior of trait implementations (for example, if you had a trait for the square-root character, the trait would require that implementations must also implement the trait for multiply and the implementation constraint would be that whatever the result of square root on t:T is, then that result r multiplied by itself (using the implementation of the multiply operator for that type) must be equal to t:T (so, square-root( t:T ) * square-root( t:T ) = t ) where "square-root" is a stand-in for the unicode square root character in this explanation.

Anything short of this involving using various combinations of ascii punctuation characters to create complex operators seems like a fools errand to create unreadable code.

4 Likes

I’m right there with you on having named functions be allowed to be infix, but let’s use backtick instead of tilde.

Also, if there’s any way to make the transformation work for both bare functions and trait methods that would be best. That part might be a little tricky, but it feels doable.

EDIT: Klingon doesn’t exist in any standard Unicode mapping :wink:

1 Like

Some previous discussion re. infix operators: https://github.com/rust-lang/rfcs/issues/1579

Personally, I think x +? y +? z reads much better than x.checked_add(y)?.checked_add(z)? and the semantics should be fairly obvious even to a reader not familiar with .checked_add IMO.

Yeah, I agree, that would be nicer. Something like?:

let a = d `cmul ( b `cadd c )  `cdiv e

Kind of "Lispy"? Or, were you thinking more like?:

let a = d `cmul` ( b `cadd` c )  `cdiv` e

The benefit of the latter is that it is you'd likely get easier parsing and perhaps cleaner error messages if you forgo a space, but, I'm not sure that's worth the extra noise. Probably would require some experimentation.

Miracles do exist! :slight_smile: