Trait operators / macro traits

Rust doesn’t have a preprocessor. That’s good because Rust offers you access to AST. That’s bad because people cannot modify code style and create new operators. In my humble opinion, adding custom operators to Rust has more pros than cons. This may solve, for example, some problems with UFCS absence:

(Probable syntax)

traitop UFCS {
macro pipe($self~($i:tt$(,$args:expr)*))$($rest:tt)*);
}

impl<T> UFCS for T {
macro pipe($self~($i:tt$(,$args:expr)*))$($rest:tt)*) {
$i($self$(,$args)*)$($rest)*
}
}

fn print<T: core::fmt::Display>(x: T) {
print!("{}", x);
}

fn main() {
16~print();
}

or unwrap operator

traitop Unwrap {
macro ($self!!$($rest:tt)*);
}

impl<T> Unwrap for Option<T> {
macro ($self!!$($rest:tt)*) {
$self.unwrap()$($rest)*
}
}

fn main() {
println!("{}", Some(0)!!);
}

There are many problems in my examples and in my (probably, not) idea - remember that’s probable syntax, not exact. I want rustaceans to take a look at this and discuss.

I disagree with this proposal. It makes code harder to read, will make it harder if not impossible to add new language features in backwards compatible ways and it is hard to implement due to things like operator precedence and associativity not being known when parsing, while it needs those properties during parsing to produce a correct parse tree.

13 Likes

Not offering custom operators was an intentional choice Rust made, both for readability and for future extensibility.

unwrap is definitely not an operation I'd want to see hidden behind an operator.

18 Likes

Beyond the "Rust explicitly decided to not have this", you're missing a few details here which is really important for custom operators:

  • associativity (i.e., a~b~c: a~(b~c) or (a~b)~c)
  • precedence (i.e., 3*4~print: (3*4)~print or 3*(4~print))

There is also have a parsing problem because any symbol soup could now be an operator. So instead of 3#$%^&*4 failing as soon as it hits the # because it's not a valid place for an attribute, the parser must now figure out how to add things to the tokenizer based on what is in scope. Rust's nice error messages may not be able to cope with such cases beyond "I dunno what you mean here, try again?".

There's also no way to handle scoping or conflicts (e.g., !! is your "unwrap", but someone else's "coerce to a bool using C/Python/JavaScript/PHP/whatever rules for values").

At a higher level, for some recommendations on any future proposals, it is best to be explicit about things. What is obvious to you may be non-obvious (or even obviously the opposite!) to others. For this proposal:

It's not clear to me why either of these things are good properties of an arbitrary language (nevermind Rust itself).

Care to be more explicit here?

5 Likes

A little (probably, not) lack of code readability is worth making future work of language implementors' and rustaceans' a little bit easier, isn't it?

I have already said probable and There are many problems in my examples and in my (probably, not) idea - remember that’s probable syntax, not exact. I want rustaceans to take a look at this and discuss.

I wanted people to discuss on how can this be implmented and is it worth all consequences, not to criticise abstract examples.

Please, I beg you, see beyond your nose and read my messages or just do not reply to threads if you had a bad day or something... Do not take it for rudeness - this is from the bottom of my heart.

Also, i understand why are you harrowed by vague doubts. That's why I wanted this to be a civilized discussion - to be a chance for people to share their thoughts. How ancient wisdom says, "in a dispute the truth is born".

Thanks.

For what it's worth, I know two languages that support (some kind of) custom operators in a manner that limits confusion to both the parser and developers/readers:

  • OCaml lets developers define operators that start with +, *, /, - e.g. +%%, *%%, etc. By definiton, +%% has the same precedence as +, *%% as *, etc. Its predecessor Caml Light (or maybe Caml Special Light, I forget) offered a more complicated variant.
  • Haskell has the backtick, which lets users infix function calls, e.g. a ``plus`` expr, is the same thing as plus a expr (or plus(a, expr) in Rust syntax). There is a single precedence rule for all operators.

Both examples are something of a hack but show that it can be done in a mostly clean way.

However, in OCaml world, the feature is mostly unused. Most developers prefer calling functions to defining custom operators because these custom operators actually tend to make things less readable. This could mean that OCaml's implementation is bad or that maybe the feature is not very useful. I suspect the latter.

I don't remember seeing the feature used in non-trivial Haskell code either, but my experience of Haskell is more limited.

My personal suspicion is that the feature would not be used overmuch in Rust if it was available. I could be wrong, of course.

6 Likes

My observations aren't about your specific examples. I did use your examples in questions that arise (namely with the precedence, associativity, and ambiguity), but this is something inherent to the idea. To be a custom operator there has to be some syntax a the usage site. There's the possibility of the Haskell `opname`syntax that I think no one would like in practice, though `opname does show up in one of the threads linked below..

And are we not discussing it? There is little provided on the "pro" side right now, sure, but who do you think should provide such information?

I did read it. Have you researched prior discussions about custom operators on here? It happens fairly regularly and the problems are almost never about how to declare such things, but how it'd actually work in Rust regardless of how you end up spelling any declarations in the code.

Some samples (some of which are about specific operators still causing problems, nevermind arbitrary ones):

I've mainly seen it in code like container `contains` item where the "operator" reads well in English with its argument order.

5 Likes

Kotlin also has infix functions though they don't need backticks. They also have a single precedence rule which can't be customized. They're used sometimes, mainly in DSLs.

1 Like

Haskell also has arbitrary custom operators, not just the backquote mechanism. You can define any operator you want in Haskell.

This gets used heavily. Some would argue it can make code more readable, if you know what the operators mean. Others would argue it can make code less readable, if you don't. I personally lean towards the latter. But in any case, it's an example of what full custom operators allows, for good or ill.

2 Likes

Scala is another language with custom operators, and I think a good example of what it looks like when you really bake this design into the language:

In Scala, operators are methods. Any method with a single parameter can be used as an infix operator. For example, + can be called with dot-notation:

10.+(1)

However, it’s easier to read as an infix operator:

10 + 1

I think it's a neat design and while I don't think something like that should (or could?) be retrofitted into Rust, it is fun to think about stuff like

use custom_operators::MyPartialEq;

let x = 1;
if x < 3 && <x as MyPartialEq>::<(3) {
    // ....
}
1 Like

In my opinion, if an operation doesn't fit to any of the preprovided operators, I find it unlikely, that an (easily typable) operator symbol would be more clear then a function or method call. This is particular clear for postfix operators: I would prefer something.unwrap() and mymatrix.adj() over something!! and mymatrix#. The benefit in typing a few characters less in these expressions is relativly minor here in particular given that many editors now support autocompletion and in terms of fitting in long mathematical expressions, the lengh of the variable name will have a much larger effect. Similarly I don't think custom prefix operators would provide any benefit over plane function or fully qualified method calls.

The only kind of operators where one could argue in favor of custom operators are binary ones, in particular associative ones. My personal usecase here, would be to distinglish between matrix and elementwise multiplication. That said, in most cases two-argument functions serve this use case just as well and I still don't find the benefits of adding custom binary to be convincing enough to accept the huge downsides (syntax ambiguity, symbol collision, additional language complexity etc).

1 Like

The Haskell support, as already mentioned, implements custom operators about as well as you could expect. Perhaps the OP could look at that and see if it meets their expectations?

I think so long as you basically treat operators like traits, eg you have to follow the orphan rule, import the declaration to use the impl, etc, there's not really any technical issues. The main complexity is the ecosystem around precedence: Haskell just asks for a 1 to 9 value, which I don't think scales to crates.io.

My bad.

Sorry for roughness, i understood you.