Any projects enabling to write Rust using a simplified syntax?

I love Rust. But sometimes it holds me back. And when starting an idea, this can be really exhausting. Rust wants you to write good, clean code, and that is the right default, but sometimes you just want to be quick and dirty. Get the code on the screen, work on it, then put it into good shape.

So imagine we had some kind of preprocessor, which takes "bad/simple" Rust code and translates it into normal Rust code. A trait definition may become

MyStruct Default default() {
     Self {
         x: 0,
         y: 0,
     }
}

, which is then translated to normal Rust for long term maintenance.

I was wondering if maybe anyone knew a macro like this, or some tool, allowing you to write simpler code and having a compiler infer what the complete definition should be. It would also bypass a lot of Rust's quality standards, which are good, but are more of a hurdle at the early stages of a codebase.

Maybe this can be thought of as something similar to derive macros, only for more applications than deriving a trait.

For example to infer the types of arguments or return types. or any types, or needed trait definitions at all.

If it's just about implementing a trait, IDEs like rust analyzer can easily fill in method signatures for you, so you'll not have to manually type anything more than in your example above (except perhaps for the words "impl" and "for"... but you save typing the word function name "default()").

Here's the keystrokes required to generate the code below with the vscode + rust-analyzer.

Keystrokes:

i<tab>Default for MyStruct<tab>d<tab>S<tab> {<enter>x: 0,<enter>y: 0,

Result:

impl Default for MyStruct {
    fn default() -> Self {
        Self {
            x: 0,
            y: 0,
        }
    }
}

I'm looking forward for the RA to be better like generating field stubs like x: in the future, but the current state is very similar with the syntax you've proposed.

6 Likes

Can you elaborate on this a bit more? I'd be quite curious to hear examples of how the syntax holds you back.

I feel like the ability to rapidly pump out quick'n'dirty code is a skill that comes with time and experience with the language/tooling, and like with any skill it's going to take a certain number of hours at the keyboard to master.

I'd argue that I'm more productive in Rust than I am in JavaScript because static typing means I don't have to keep large swathes of my program in my head to avoid unnecessary errors like "undefined is not a function". Then, because rust-analyzer knows all the types and what is required, it can provide code actions like "fill in missing trait methods", "add missing match arms", "generate function" (automatically infers argument/return types) and "add return type".

9 Likes

You can also complete MyStruct with e.g. M<ctrl-tab><enter> (if it's the first completion starting with M.


You can also fill the default default implementation if you’re at

impl Default for MyStruct {
    <cursor here>
}

press

<ctrl-.><downarrow><enter>

and you get

impl Default for MyStruct {
    fn default() -> Self {
        Self { x: Default::default(), y: Default::default() }
    }
}

If you don’t do that, instead once you’re at

impl Default for MyStruct {
    fn default() -> Self {
        Self {<cursor here>}
    }
}

you can

<ctrl-.><downarrow><enter>

to get

impl Default for MyStruct {
    fn default() -> Self {
        Self { x: todo!(), y: todo!() }
    }
}
4 Likes

Rust is not a rapid prototyping language. If you want a rapid prototying language, there are plenty to choose from. I like to use Python for that purpose. Then, once you have a clear idea of what you want to do, you can rewrite the existing minimal code in Rust.

1 Like

Thanks for all the suggestions, they already help speed up the coding a lot, and I probably don't use them to their full extent.

Especially when dealing with generic code or type level programming, I still need to repeat myself very often. When I have a generic struct and change something about its generic arguments, for example I add a type parameter, lifetime or a trait bound to a parameter, I have to repeat this for every implementation for this struct:

struct Generic<T: Bound> {}
impl<T: Bound> Generic<T> {
    ...
}
impl<T: Bound> TraitX for Generic<T> {
    ...
}
impl<T: Bound> TraitY for Generic<T> {
    ...
}

Now when I change something, I have to repeat that 3 times already. For trait bounds I usually use something like "trait aliases":

trait MyAlias : BoundA + BoundB {}
impl<T: BoundA + BoundB> MyAlias for T {}

So here I can change the bounds in one place. But adding or removing a type parameter still takes a lot of changes for large generic types. I find this becomes more and more tedious the more you use generics and traits, and it should probably be addressed.

Maybe rust-analyzer has some tool for this aswell?

You got a point, but it would be nice if we could basically automatically translate "pythonic" code into "rustic" code. Also, I actually like the expression level language of Rust much better. It is already very concise and much more clear imo. Also the libraries are different, so rewriting a Python script in Rust might need a very different approach.

Maybe it is not really possible to do, but I was thinking a language, that doesn't even need to compile to running code, but just allows for much more simple syntax and basically lets you ignore types for a while, would go a long way. So you can start drafting the code structure in this language, then translate it, where all the typing gets filled in and you resolve possible problems, and then you get running Rust code.

Maybe it would be nice, but I don't think it's realistically possible. Personally, I don't even prefer Python's whitespace-sensitive syntax, I find it harder to read and way more error prone.

Agreed.

Not a "very different" approach; but you might need to fill in the gaps of missing libraries. E.g. I have done that with a signal processing heavy project recently — I had to implement IIR filters from scratch in Rust. It was totally worth the effort though, in terms of performance, and it obviously didn't impact the amount of effort I needed to put into the Python prototype. Also, writing missing libraries for Rust doesn't require an alternative syntax to be added to Rust.

3 Likes

For purely syntactical changes, you could use build.rs to convert your syntax to standard Rust. It will have issues with source locations unfortunately.

Procedural macros aren't feasible for this yet. If you stick to a syntax close-enough to Rust for the file to tokenize as Rust, you could attach proc macros to individual items. However, having a macro work on entire files or folders is not feasible, because stable proc macros can't create spans for arbitrary files/locations yet.

As for Rust itself, the syntax is stabilized, and won't change. Your best option is to learn how to live with it (get used to it, or use IDE with snippets, auto-complete, etc.).

4 Likes

This specific case of redundant generic bounds is actually the subject of an accepted RFC: 2089-implied-bounds - The Rust RFC Book

Unfortunately it doesn't look like much implementation work has been done on it. :frowning:

2 Likes

Luckily "real" trait aliases exist on nightly, https://doc.rust-lang.org/nightly/unstable-book/language-features/trait-alias.html, so hopefully that'll get stabilized in the not-too-distant future.

This one seems like it's just expected, for a statically-typed language. It also takes a lot of changes to add struct fields, enum variants, function parameters, etc.

(Maybe there are a few places this could be simplified, like how there's .. in patterns, but in general this breaking is considered a feature.)

It's probably one of the things waiting on chalk integration before people want to make big inference changes.

There's also an unresolved question in there that's very important to me, which is about how it changes which changes are semver compatible. Right now it's semver-compatible to change struct Foo<T: Copy>(...); to struct Foo<T: Clone>(...);, because all the uses in 3rd-party code have T: Copy. But if they didn't have that "redundant" bound, then it'd be a breaking change, which would be a shame.

Not to mention that it's too late for HashMap to become struct HashMap<K: Eq + Hash, V> { ... } anyway -- even with implied bounds that's a breaking change, AFAIK. (Unless not-in-the-signature uses of HashMap changes the bounds on type parameters, but that sounds like it's very much against a bunch of other common Rust principles.) So it'd be a bit unfortunate to add a feature that std can't really use in the "obvious" places.

Not to mention that it's not always clear that putting the bounds in the type is the right thing to do even with implied bounds. Back in 1.0.0 Cell's methods only worked with T: Copy, so it would have been really tempting to make it struct Cell<T: Copy> { ... }. But it's very good that it's not, since modern Cell has all kinda of methods that's don't need T: Copy.

(Admittedly the calculus outside the standard library could be somewhat different here, with breaking changes being more feasible and the bounds often being more involved. But I still think it'll be important to find the right middle ground between what we have now and the maximal "invisible bounds" formulation.)

4 Likes

This topic was automatically closed 90 days after the last reply. We invite you to open a new topic if you have further questions or comments.