Is there a way to customize operators and symbols?

As I wrote a lot of match these days and the arms => are a bit tiring to type, I wonder if there is a way to customize operators and symbols? For example, I'd like to replace => with then which is easier to type for me. I don't know much about macros, but can macros do that?

No, Rust doesn't allow you to replace arbitrary symbols in the syntax like this. Some operators can be overloaded, such as + and -. This is done via the traits in

In addition, you can use macros for defining pretty much any arbitrary syntax that you want, within a certain scope. (Subject to some very mild and sensible restrictions, such as the requirement of matching parentheses.) However, redefining core syntax is not idiomatic, and will confuse other readers of the code, so it's generally not recommended.

I think if codes are wrapped in a macro, I can replace all then I type with conventional => so to pass the compilation, like below

a_macro{
   match sth{
   A then { }
   B then { }
   _ then { }
  }
}

but is there a way to get rid of this macro wrapping braces {}, so I can do like

use a_macro // or some other ways to state that I want to replace `then` I typed with `=>` 

match sth{
  A then {}
  B then {}
  _ then {}
}

Yep, although it's probably not going to be easy with a traditional macro-by-example.

No, not really. However, you could write an attribute proc-macro which introduces this syntax in an entire function:

#[match_then]
fn my_function(x: i32) -> u32 {
    match x {
        0 then 1,
        1 then 0,
    }
}

It sounds like you might want to add a macro/shortcut/abbreviation/whatever-you-call-it to your preferred editor, so that you can type something easier that will be automatically replaced with a =>. For example, in Vim I can set

:iabbrev then =>

and after doing that, any time I type then followed by a space, it is replaced with =>. (You might want to use an abbreviation that isn't a common word, like thn, so you don't accidentally trigger it when writing comments.) I only know Vim, but any editor worth using should be able to manage simple substitutions like this.

2 Likes

Yes! the problem is exactly what you said. Besides, if we can (sort of) customize the syntax like ideally what I imagine, we can (sort of) distribute a dialect via distributing a macro lib without breaking the foundations of this language. I think it is kind of interesting, because we can be able to do "syntax-level meta-programming"

Out of curiosity, what type of code are you writing and why does it force you to use so many match statements?

I feel like instead of making match require less key strokes, it may be better to X-Y the problem and just reduce the need to write them so much.

If this is just about replacing => with then, you may want to check out the thread on using ~ instead of mut.

Overall, the consensus was that changing a language's syntax for cosmetic reasons isn't worth it due to implementation costs, duplicate syntax, having to rewrite all your training material, confusion over how to use match, people seeing it as a Python 2 vs Python 3 breakage and all the negative PR that entails, etc.

It'd also be like going to the implementors of Python and saying "I love Python, but can we use curly braces instead of indentation?" Which is a hard sell.

2 Likes

I understand the concern and I also agree that syntax stability is crucial. But, as the discussion continues, I wonder(just out of curiosity) whether it is possible that we can do something like type a_type = b_type, say symbol => = then, so the compiler can actually replace then with => during preprocessing. Such definition can be distributed in libs, can be understood by the compiler and can even be removed by a preprocessor so that when releasing source code, developers can choose to get rid of these aliases and publish it in standard syntax.

As required by my CG homework, I have to code a rasterizer and a ray tracer from totally scratch (from Linear Algebra), so when dealing with dimension mismatch, I need to return many Result s, which means there are many match as I prefer match to if and else checking.

If you are just checking for an error and bailing from the function, you can probably drop a lot of those match statements by using the ? operator.

That means code like this

fn read_username_from_file() -> Result<String, io::Error> {
    let f = File::open("username.txt");

    let mut f = match f {
        Ok(file) => file,
        Err(e) => return Err(e),
    };

    let mut s = String::new();

    match f.read_to_string(&mut s) {
        Ok(_) => Ok(s),
        Err(e) => Err(e),
    }
}

Will be simplified down to this

fn read_username_from_file() -> Result<String, io::Error> {
    let mut f = File::open("username.txt")?;
    let mut s = String::new();

    f.read_to_string(&mut s)?;

    Ok(s)
}

Does that help?

1 Like

Yes, I could, but this will propagate the error to upper levels, but I'd like to handle them properly as locally as possible.

I can relate to that sentiment.

There are a couple other techniques you can use to reduce the monotony of error handling.

I prefer to make sure the code is correct by construction so it's just not possible to introduce errors. You can do that by passing around fixed-size arrays or structs when you know you'll always have a fixed number of items.

You can also do this by creating a type which will internally uphold your invariants and not provide any methods that could break them. An example of this is std::path::PathBuf which is just a wrapper around an OsString that only provides methods for adding/removing path segments. It maintains correctness by not giving you free reign to mutate the internal buffer (which could result in an invalid path).

There are also situations where you know something will always be correct, but the type system can't guarantee that. In these scenarios it's perfectly fine to use an assertion and panic if that assumption was incorrect. For example:

let vector = vec![42]; 
let first_item = vector.first().expect("unreachable: The vector was initialized with 1 item");

The unwrap() and expect() methods get a lot of flack because people use them when they are too lazy to handle errors. However, sometimes you genuinely believe an error case is unreachable, and if that isn't the case the world is fubar and you want to crash loudly to let the developers know their underlying assumptions were incorrect.

2 Likes

Yes, it's reasonable, unwrap() and expect() are good when we are assertive (or lazy lol). But I think it's slightly off the topic. If we can do something like symbol => = then just like type type_a = type_b, then lazy customizers (like me lol) and purists will be both happy, I think

I don't recommend it, but you can always write a Makefile that passes your Rust source code through the C preprocessor before invoking cargo. Then you can #define then => and be on your way.

That reminds me of the person who wrote the Bourne shell.

He didn't like C's syntax so he created a bunch of #defines to "fix" the language and make it look like Algol. The end result was code that looks like this, which feels incredibly bizarre to most people familiar with C.

3 Likes

Thanks! It seems working, but I think it is not portable, so I am now trying to do macros besides that.

I think the problem in this is the #define will not be check anyway, so the compiler does not know anything, which makes it unusable, but if a compiler can support sort of well-checked syntax re-definition, it will be much helpful I think