Propose new operators to reserve / include for edition 2018

We could switch from .. to ..< for half open range and from ..= to ... for closed/inclusive range. IMO, Swift got this right.

1 Like

This was bikeshedded to death; let's please not reopen it.

6 Likes

I understand the sentiment and I’m not going to push for any changes to stable syntax. The only reason I feel the need to mention it is because changes to stable syntax were previously unthinkable. Now they’re not. Please note my careful wording: “could.”

EDIT: Apologies if this was discussed after the epoch RFC was accepted and I didn’t see it.

I would add a checked indexing operator too : [?.

pub trait TryIndex<Idx> where Idx: ?Sized, {
    type Output: ?Sized;
    type Error;
    fn try_index(&self, index: Idx) -> Result<&Self::Output, Self::Error>;
}

variable[?10] would be sugar to variable.try_index(10)?.

The most obvious new operators for me would be:

  • Euclidean modulo / division.
    • These should probably be more commonly used than the % and / operators, since they have much nicer mathematical properties.
    • Traits could be called Mod or Modulo and EucDiv or FloorDiv.
    • I suggest we could repurpose the mod keyword so you can write
      • let x = -3 mod 4; // x = 1
      • let x = -3 div 4; // x = -1 (potentially use backticks here? `div`?)
  • Wrapping operators.
    • These currently are very verbose and quite needed for a lot of applications. To make the code neater you can wrap the types but that’s pretty awkward too. Sometimes I just use regular operations to make it look neater when the real operation I wanted was wrapping, causing potential bugs in debug mode
    • Traits would be WrappingAdd etc.
    • Simplest is to combine a common sigil, say % with the existing operators, so you would have
      • let x = 255u8 +% 2; // x = 1
      • let x = 254u8 *% 4; // x = 248
  • Conversion operator
    • Something simpler to write than TryFrom::try_from for converting between types
    • Trait would be From / TryFrom
    • Syntax bikeshed:
      • let x = 19u32 as? u8; // x = Ok(19)
      • let x = 256u32 as! u8; // error, no implementation of From
6 Likes

@kornel @varkor @djzin My initial vehicle for learning Rust (late last year) was to code variants of a strong cryptographic algorithm that is lightweight enough for retrofit embedded use. The lack of lexemes for

  • add, sub, and mul for unsigned finite-ring arithmetic (e.g., u32)
  • as well as rotate_left and rotate_right
  • and the assign variants for these five operators

led me to delve into the compiler and work out the many changes that would be needed to add such lexemes and their associated trait language items.

The lexemes I considered for finite-ring arithmetic were +%, -%, *%, +%=, -%=, and *%=; those for rotate were <<<, >>>, <<<=, and >>>=. For Rust, I saw some advantages for using the trailing % rather than Swift’s leading &:

  • the trailing % expresses the order of operation: first add/sub/mul, then take the remainder of the result (as an unsigned integer) modulo the representation size, then optionally assign that value to the first operand;

  • lexeme augmentation with the % suffix makes less jarring the potential inclusion of Euclidian modulo and associated Euclidian divide operators (%% and \%, respectively).

  • the & sigil used in Swift’s finite-ring operators is already used heavily in Rust, more than other languages, due to its use as the ref operator, so not using it for the finite-ring add/sub/mul operators reduces potential confusion

I personally feel that Swift’s &+, &-, and &* operators emphasize the “bit-fiddling” aspects of the operations while suppressing awareness that the underlying arithmetic has moved from the infinite ring of all integers to the finite ring of all integers with a given representation. For the same reason, I looked at what it would take to limit the new lexemes to unsigned arithmetic, so that applying the existing wrapping operators to signed integers (which are operations in a very different finite ring) would always require more-verbose programming effort. (Unfortunately, I concluded that tracking known signedness of integer expressions would require extensive changes to the compiler. That conclusion also implied that new lints for distinguishing use of wrapping and rotation operators on signed values would also be difficult.)

FWIW, I eventually decided that cryptographic primitives should use the approach found in the octavo crate, declaring the needed finite-ring types as wN (rather than using uN), together with the mathematically-appropriate traditional +, -, and * operators and the W type-conversion operator for constants.

5 Likes

Since I'm unfamiliar with octavo, how (if at all) do its wN types differ from type wN = Wrapping<uN>;?

They don’t differ at all. The link in my post simply shows how that particular crate makes use of the wrapping trait more palatable. The key element for me is that use of

pub use core::num::Wrapping as W;

#[allow(non_camel_case_types)]
pub type w32 = W<u32>;
#[allow(non_camel_case_types)]
pub type w64 = W<u64>;

shifts the conceptualization of the crypto algorithms from the infinite ring of all integers, albeit constrained to u32 or u64 (subject to representation overflow or saturation), to the finite rings of 2^32 and 2^64 elements. It’s the clarity in mathematical conceptualization that I find attractive. Use of w32, w64 and typed constants such as W(0x9b05688c2b3e6c1f) let Rust’s type-checking detect accidental conflation of the rings. This benefit doesn’t accrue with u32 and u64 and use of lexemes such as *% for the wrapping operators on those infinite-ring type values.

2 Likes

What i think would be a neat addition to the language are the monoid operators (+) and (*).

This would use two traits: MonoidAdd and MonoidMul. Both traits define the operator function and an id function.

Why? Monoids are extremely useful. Most objects we deal with while programming can be expressed as monoids. This would give us two dedicated all purpose operators for library code, which would (imo) be a sweet spot between an explosion of domain specific operators and misuse of existing operators for other purposes.

Why two monoids? Several reasons. There are some semantic connotations with (+) and (*), so libs can choose the most fitting one. For example list concatenation would use (+). Matrix multiplication would use (*).

Also, there are often more then one important operations that can be used in a monoid. And most importantly, this would allow the definition of a ring structure. I would imagine a library to augment this traits with traits for things like commutativity and additional mathematical structures like groups and rings and a bunch of general methods to operate on said structures.

Why the tokens (+) and (*)? An obvious alternative for the first one would be ++. But ** is problematic because infix * before prefix * is already legal (for example 2**&2). Also i quite like how (+) resembles the mathematical notation of circled plus and multiplication.

I think this would get us a lot of bang for the buck in terms of generality. The cost would be the introduction of a fairly mathy concept into the language and one could argue that that is not very inclusive to people without formal computer science education. On the other hand, in other areas rust doesn’t shy away from theory when its useful and everyone who has studied one or two semesters of computer science, engineering or any other math-adjacent course has seen this before. And “use (+) to concatenate lists and to merge sets” doesn’t require theoretical understanding.

Sorry for the wall of text. If there is any interest in that, i could open a tread.

6 Likes

Maybe this can be combined with @Uther’s proposal for checked arithmetic. Something like +!, -!. /!, *!.

Edit: I just saw the proposal of using % as suffix - which makes totally sense after a bit of thinking :slight_smile: - two new operators as% / as? would would fit nicely into this scheme.

1 Like

For what its worth, a way to write this that you will either love or hate is a[n..][..3] (for the positive case).

1 Like

My original idea was to propose operators with exclamation mark too, but it would be inconsistent with negation syntax.

Would value1+!value2 means (value1) + (!value1) or would it means (value1) +! (value2)

uther, not an expert but i think it means (value1) + (!value1), but i can be wrong. will appreciate more experienced people to answer this. i am in search to buy hgh that’s why it is hard for me now.

This was a rhetorical question, since currently the +! syntax does not exist. Currently, it obviously means (value1) + (!value2).

That’s why introducing the ̀+!` operator would be a problem. It would either break badly a lot of current code, or it would be unusable without always using parenthesis on the right side.

1 Like

Something that composes and decomposes tuples would be nice…(not necessarily new

I would just like for Index to be able to return a Proxy.

4 Likes

+ is usually commutative in mathematics (with one exception: ordinal arithmetic), so I don't think it would fit lists. The monoid operation of free monoids – the usual formalisation of lists in algebra – is usually written as juxtaposition, IIRC; '+' is preferred for abelian groups. As for sets, I think reusing bitwise operators would fit them better (| for union, & for intersection, ~ for complement): in a sense, bitwise operators already reinterpret a number as a set, after all.

2 Likes

I’m not sure how serious I am about this, but… how about infimum and supremum operators? For total orders, those are synonymous with minimum and maximum (i.e. for Ord types, they would reduce to std::cmp::min and std::cmp::max), but potentially they could be used much more generally:

  • A set type could define infimum as set intersection and supremum as set union;
  • A (grid-aligned) rectangle type could define infimum as intersection of rectangles, and supremum as the smallest rectangle surrounding its operands;
  • A list type could define infimum as the longest common prefix of its operands;
  • A ‘division lattice’ wrapper for integer types could define infimum as GCD and supremum as LCM;

and so on. I do realise bitwise operators can be semantically similar at times (as I noticed in my previous post), but I think having dedicated operators known (or at least expected) to agree with the order relation on the type would be much more useful.

Also, given that attempting to re-use ASCII punctuation for new tokens can give rise to so many incompatibilities and syntactic ambiguities… how do you feel about using non-ASCII characters for new operators?

With both in mind: traits Meet for (infimum) and Join for (supremum). (Names come from algebra; Inf and Sup could also work.) Or you could have them spelled the way they almost were in K&R C: /\ and \/.

2 Likes

You alrady know my opinion about this, but this is a different thread and I also have something new to add. I propose that we don’t add more operators to Rust. The language’s direction would be better off without more magic punctuation.

Early Rust used to have lots of special symbols, way more than it was reasonable. It has been fixed before the 1.0 release by removing or replacing most of them (e.g. ~ and Box), and only leaving those that were immediately recognizable.

This was a community decision, people preferred actual readability and easy searchability over “terseness above everything else”.

10 Likes

I’m with @H2CO3’s sentiment, that “magic punctuation” can be a real burden for code clarity

I like

  • The checked arithmetic operators (+?, *?, etc.) proposed by @Uther because they’re essentially a combination of two existing concepts: The usual arithmetic operators and the try operator (?). But ONLY if they are 100% equivalent to a.checked_add(b)? etc. (This means that the Try trait implemented on Option would need to be stabilized first) In any case, reserving these as operators can’t hurt.
  • The mod operator. It’d be exactly the same notation as in mathematics and mod is already a keyword. It doesn’t fall into the “magical punctuation” category because every programmer who sees it immediately knows what it means.

I don’t like so much

  • The monoid operators (+), (*), etc. IMO they are very niche. Libraries could just define the same ops with the existing +, * etc. and it’d be fine. I think that a special notation with only the purpose to emphasize mathematical connotations is not enough of a reason to introduce new operators into the language.

Here’s an idea Maybe Rust could get custom binary operators which are just normal methods with an annotation that enables an additional calling style for them. It would avoid the introduction of several weird special character operators like !==~ to do whatever.

impl Vector3 {
  #[binary_operator]
  fn dot (&self, v2: Vector3) -> Vector3 { ... }
}

let value = (vec1 dot vec2) dot vec3; // New

let value = vec1.dot(vec2).dot(vec3); // Current way
2 Likes