Which Lifetime & Type Sugar Is Cleaner And Easier To Read? Which One Is Best Way To Build Extension To Remove Semicolon?

Hello, I'm adding lifetime sugar to my sugar collections. But I'm confused which one is the most cleaner, easier to read, clear separation, doesn't convolute the code once there are many of them. I would like to hear your opinion

I realize that the tree structure contributes to the confusion after there are deep hierarchy type, like Result<Vec<HashMap<String, HashMap<String, i32>>>, Error. Thus combined with lifetime looks more convoluted. So what if it becomes linear flat, like Result<_, Error> | Vec | HashMap<String, _> | HashMap<String, _> | i32

Vec | i32 means Vec has i32
HashMap<String, _> | i32 means the _ is i32

Now updated the code, what do you think if the lifetime sugar combined with linear flat type like this?

Here is the list

NOTE :

  • pseudocode, not the detail implementation
  • it is to show what it looks like if in the case that needs multiple lifetime with nested (multi reference)
  • what it looks like in case there is deep tree type
  • what it looks like if combined with generic
  1. Scope = variable is dropped after this name. R! = macro of lifetime + reference (&'). Type is the last
#[fn(
    scope : config, user, table
    type :
        T
        U = Allocator + MyTrait
)]
fn proses_data(
    cfg: R!(config, str),
    user: R!(user, R!(config, str)),
    table: R!(data, 
                  HashMap<
                      R1!(config, str),
                      R1!(config, T)
                  >
              ),
    complex: Result<_, Error> | R!(user, HashMap<String, _>) | R!(MaybeUninit<_>) | [_; 100] | U) -> R!(user, str) {
    *user
}
  1. Declare scope name, and call them with / (breadcrumb). Lifetime on left, type on right
#[fn]
scope<config, user, data>
fn proses_data<T, U: Allocator + MyTrait>(
    cfg: /config &str,
    user: /user/config &&str,
    table: /data &HashMap(/config &str, /config T),
    complex: Result(_, Error) | /user &HashMap(String, _) | &MaybeUninit(_) | [_; 100] | U,
) -> /user str {
    *user
}
  1. Almost same like before, except it is grouped with [..]
#[fn]
fn proses_data<T, U: Allocator + MyTrait>(
    cfg: [config] &str,
    user: [user, config] &&str,
    table: [data] &HashMap<[config] &str, [config] T>,
    complex: Result(_, Error) | [user] &HashMap(String, _) | &MaybeUninit(_) | [_; 100] | U,
) -> [user] &str {
    *user
}
  1. Type remain clean, lifetime is defined in lifetime clause similar to where clause
#[fn]
fn proses_data<T, U: Allocator + MyTrait>(
    cfg: &str,
    user: &&str,
    table: &HashMap<&str, T>,
    complex: Result(_, Error) | &HashMap(String, _) | &MaybeUninit(_) | [_; 100] | U,
) -> &str
lifetime
    cfg: config,
    user: user config,
    table: data<config, config>,
    complex: Result<_, _> | user<_, _> | _ | _ | _,
    return: user
{
    *user
}
  1. Declare scope and the relationship cleanly at the top. Generic type on the last. Use scope with #, lifetime on the left, type on the right, every type with # means it's reference.
#[fn(
    scope : config, user, table
    type :
        T
        U = Allocator + MyTrait
)]
fn proses_data(
    cfg: config#str,
    user: user#config#str,
    table: data#HashMap(config#str, config#T),
    complex: Result(_, Error) | user#HashMap(String, _) | MaybeUninit | [_; 100] | U,
) -> user#str {
    *user
}

Each of them represent this :

fn proses<'config, 'user, 'table, T, U>(
    cfg: &'config str,
    user: &'user &'config str,
    table: &'data HashMap<&'config str, &'config T>,
    complex: Result<Vec<HashMap<String, MaybeUninit<[U; 100]>>>, Error>) -> &'user str
where
    U: Allocator + MyTrait
{
    *user
}

I am also looking for a way to build extension to make can write Rust without ; similar like those in Typescript, Kotlin, Swift, Golang

I have idea like this

cargo-auto -> rewrite code with auto insert ; to different folder -> compile that one

But I am looking the method without rewriting if possible to prevent duplicated target folder. And does not trigger Rust analyzer IDE inline error show

Is there any?

oh wow, really interesting

tbh all options look unreadable :sweat_smile:

Honestly, the Rust code.

That may partially be familiarity... but it's familiarity shared with every Rust user.

At least format the rust code for a proper comparison:

fn proses<'config, 'user, 'table>(
    cfg: &'config str,
    user: &'user &'config str,
    table: &'table HashMap<&'config str, &'config str>,
) -> &'user str {
    *user
}

I should note that this is a pretty poorly motivated example, since you can elide nearly everything and use just:

fn proses<'user>(
    cfg: &str,
    user: &'user &str,
    table: &HashMap<&str, &str>,
) -> &'user str {
    *user
}

with no loss of generality. There's much more interesting "syn-tax" in Rust for things like trait being enumeration and async lifetime capture bounds if you were serious about improving syntax, but that's a much steeper hill to climb.

Of them all I'd say 3, but imo rust syntax is better.

I realized that the tree like diamond type becomes confusing if there is deep hierarchy. I added new sugar (linear flat type), and combined with the lifetime. What do you think about that? :>

I am also looking for a way to build extension to make can write Rust without ; similar like those in Typescript, Kotlin, Swift, Golang

I have idea like this

cargo-auto -> rewrite code with auto insert ; to different folder -> compile that one

But I am looking the method without rewriting if possible to prevent duplicated target folder. And does not trigger Rust analyzer IDE inline error show

Is there any?

That's either a proc macro or a build script, using the cargo provided OUT_DIR to handle target dir locking, is my guess for the closest thing it sounds kind you're asking for, but there's a bunch more you'd need to provide for this to be actually effective, such as a compiler front-end to remap errors to the source location and editor services (using a well done proc macro somewhat handles this for you)

On top of all those considerations, note that the presence of semicolon has a semantic meaning in Rust already; the last item in a block is implicitly the value of the block if it lacks a semicolon. There's some ways you could handle that, but you'd need type information for the nicer ones, which raises the bar a lot

Thank you, macro seems the better way and easily portable, I just need to extend the #[fn] macro then. For the return without keyword return, my algorithm is if it is at the end don't add semicolon. But I feel like prefer to remove it too aka must type keyword return for clarity. The difficult part is yes like you said, how to make the error line info represent the original code, not the proc macro project >,<. I will search way for this with LLM. At least I already know I will use macro

My motivation to remove semicolon and that whole sugar is because the current viral tweet that Rust syntax is ugly. Which I kinda agree >,<. Removing 1 noise will improve readability in many line of code

Eh, ugly syntax is often just a factor of familiarity as already mentioned. If you've used more languages than just C and Java the trailing type annotations (a common complaint) is much nicer to deal with, the consistent item / expr / block behavior is a delight compared to just about any traditional expr / statement language, etc.

I have a few complaints (I'm very much not a fan of the lambda syntax, fn items not being excluded from the last item return rule, the complexity added by using <> for generics, fn return type using -> instead of :, ...), but overall I'd put it pretty high up.

Imo getting input whether it is ugly or not from someone that already familiar with the language is not objective approach, it would consist bias of already know the thing. The most objective one would be taking someone that doesn't know the language yet, ask show them a little of many code example. That is why there is language that people easy to click with it, and hard to click with it

The tree diamond a<b<c, e<f>, i>, z> is hard to scan if it scales to complex. It introduces cognitive load because to scan the start < and then jump to find the end > to determine which one the child that I search, that child has what siblings

Reading thing liniearly naturally be understood easier even almost without effort by brain, where reading thing that jump requires effort

Semicolon is also noise, after technique to create language without semicolon is discovered. It put unnecerery particle when be combined with many other code it looks something convoluted from the top. Reading word that is clean is easier than word that consist of many symbol, look fresh to the eye

Modern languages removes semicolon but keep block, it is unfortunate that Rust grammar was already created to require semicolon >,<. I wonder would it possible to make it work with or without semicolon, like Javascript and Odin previously it requires semicolon then it was made optional can remove semicolon

I am deciding the stack I want to use to create the macro. Do you have library stack option for creating proc macro that you know it is easiest possible to use and compiles fast? Not syn because it is slow

I suggest you get yourself familiar with the concept of expression-based languages. The semicolon is not a styling decision, it plays a critical role to separate expressions from statements, as simonbuchan already pointed out.

It is true that an indentation-based solution could have been chosen instead, but then you are only exchanging one syntax for another, and it's arguably which one is better. Personally, I don't like indentation-based syntax, I find it harder to parse.

I never mentioned indent based languages (Pyhon, or other indentation language that I don't know, what I know is Python). I clearly mentioned Typescript, Kotlin, Swift, Golang, Odin. None of it uses indentation, and none of it requires to type semicolon. I mentioned that languages for a hint how they can be done without requiring semicolon on user space code despite using bracket block { .. }. I even mentioned Javascript and Odin that previously required semicolon on user space code, now does not requires it. I thought you would already browsed how they did it, I didn't expect indentation because clearly none of the example languages that I mentioned uses indentation. The answer that I just get as I was browsing is Semicolon Inference, or another browsing result also call it as ASI (Automatic Semicolon Insertion)

Here's a blog post by Matklad on this subject. It's a few years old but holds up at least in principle.

Edit: I have some memory of seeing another post where someone else minimizes Rust as a thought experiment but I can't find it now.

Getting feedback from people that don't know the language is also not objective, and in fact it's also quite easy to argue it's significantly less objective!

As I mentioned specifically:

That is, one of the more common complaints is something that pretty much every single language except those two (and their very deliberately similar offshoots like C++ and C#) has, and a more diverse experience with languages would show out C and Java as the weird-looking ones!

Many of the other odd-looking syntax in Rust is similarly only odd-looking if you're unfamiliar with their influences: largely OCaml and related functional languages; especially for functionality like type-classes (traits) and pattern matching. Compare C#'s pattern matching syntax and tell me Rust is not far more consistent and simple!

In order to make a serious claim about objectivity, you would need to have some sort of double-blind comparative study of a bunch of people completely new to any programming at all, where you compare how well they learn various concepts across different language syntax variants for the same underlying language.


I'm not really sure what you're after with:

Syn isn't actually that slow: ~1-2s to compile a complete Rust parser isn't all that shabby, though I'm sure it could be better.

But more importantly for you, it parses Rust syntax, so I can't see how it would be of any use at all for you regardless. If you want to change how Rust gets parsed, you're writing some kind of a parser!

Strictly I suppose you could be a completely rules-based system that injects ; between certain tokens, but then you don't need anything at all: just read in tokens from the token stream and write them back out, and inject your own when you think it should be there. But I don't think you're going to be able to pull that off successfully without at least enough of a parser to be able to at least distinguish between things like structure literals / patterns and blocks.

Let's not make comparisons to Javascript. JS allows not using semi-colons but there are plenty of cases where not using semicolons will result in code not doing what you expect and changes in white space can break things.
For example: https://medium.com/@tolulope-malomo/the-javascript-bug-from-hell-01bb1670d7ae

It is not semantic, but the syntax is not clean (lifetime, generic, multi trait bound, semicolon, macro, deep tree diamond)

It is more objective like if they can understand it quickly, it is good user experience design

Not comparing with the worser one here, because there is no point comparing with the worser one, just to make feel better. But comparing with the better one in certain aspect, then trying to make Rust also become better one is that aspect, in thus case is clean readable syntax

Just finished the macro, then it doesn't work >,<. It looks like the compiler rejects custom syntax because it is not valid Rust grammar as soon as possible at the first stage before macro expansion step, is it true? In this case is attribute macro

It looks like custom syntax can only survive until macro expansion stage if it is placed inside macro parameter direactly, like #macro[....], macro!(...) and macro! {...}. Not the code that the attribute macro process

If I wrap the entire code with macro! { }. It works, but it deactivate the syntax highlight >,<

Is there a way to make the syntax highlight also works on code inside macro, including auto format, and any other Rust analyzer feature?

Or do you have any other advise?