Hacker News new | ask | show | jobs
by malcolmstill 1203 days ago
There are a number of sources of noise in rust, but the one I find most annoying (because it's also so common) is the double colon. My current theory is that it's because the colon is the same height as lowercase letters.

If you end up with a long::run::of::module::names, I find it all just blurs into one.

3 comments

Okay but who is writing code like this instead of using `use`?

Also this is an no-win situation. C++, Ruby, Perl, and others have used `::` as module-scoping syntax for decades. If Rust does something novel, it's penalized for being unfamiliar. If Rust uses syntax for which there's ample prior art, it's apparently line noise. If rust used a `.` as a separator, it's unclear if you're descending into modules or calling a function chain.

There's literally no way to win.

If an import is used rarely, then i prefer qualifying paths instead of importing them.

This is especially true when there's similarly named items from different paths. Best example is `Result`/`Error` types. eg: std::io::Result vs normal default Result vs other crate's Result type. Another example would be math types like Vec2 between game engine and egui. String in mlua vs std rust etc..

Usually you can get around this by importing it with a different name like `use mlua::String as LuaString`. but it is still something that you need to actively do.

I did use the example of long run of module names, and I do take your point about `use` (though I have to sometimes read the very top of a file too!)...but the same applies to a lesser extent to the "last mile" module (e.g. `String::from`, `Vec::new`) which will be seen throughout any rust code.

> If rust used a `.` as a separator, it's unclear if you're descending into modules or calling a function chain

Interestingly, from the zig documentation:

    Zig source files are implicitly structs, with a name equal to the file's basename with the extension truncated. @import returns the struct type corresponding to the file.
This means that there isn't really any difference between accessing a struct field and accessing something in module...the module is also a kind of struct (and rust, as in zig, differentiating struct field access vs function invocation is possible because of the parens in the latter).
> There's literally no way to win.

Yes there is? Follow what C#, F#, Java, TypeScript, etc. do

That is a very interesting observation. I am starting to have a similar distaste for the visual noise caused by the ':' in my fully type-annotated Python code. This is particularly noticeable after spending a few weeks writing Golang, then going back to my Python code. The Go code feels far cleaner syntactically and visually.
I wonder if this could be ameliorated through font choice and highlighting.
can you please point out a few more annoyances in rust syntax? I am a newbie playing with language design and could use some perspective :)
To be honest that's the major one for me. Someone else has mentioned lifetime annotations, but those uncommon enough that I don't find it much of an issue.

If I was to think about it: comparing rust to zig, zig benefits a lot from error and optional types having their own syntax rather than being treated like any other type. For example a return type of:

    Result<Option<usize>, Error>>
in rust is rendered (mostly) equivalently in zig as

    !?usize
Note: there are some options in rust for cleaning up errors such as the anyhow library.

Disclaimer: I'm a zig fan and contribute monetarily so please weight anything I say on zig vs rust with that in mind. (I do write rust at work though)

i'm curious, how do you specify the error type in the zig version?
So usually you don't have to specify the error type. The Zig compiler works out what errors are returned from the function by looking at any errors returned directly or errors returned from other functions called within the body of the function. That set of errors forms an enum that is the actual error type, you just don't have to write that out explicitly. An example might be:

    fn someFunction(a: usize) !usize {
        if (a < 10) return error.LessThanTen;
    
        const b = try anotherFunction(a);
    
        return 2 * b;
    }
    
    fn anotherFunction(x: usize) !usize {
        if (x < 20) return error.LessThanTwenty;
    
        return x * 3;
    }
The compiler infers the error type of `someFunction` as:

    error {
        LessThanTen,
        LessThanTwenty
    }
When you then `switch` on an error type, the compiler will exhaustively check that you have handled all the cases.

Note the "(mostly) equivalently" was a reference to the fact that Zig errors can't (currently) contain any other information, whereas an error in rust can carry other information.

Also note that the compiler can't infer the error type in all case, for example in the case of a recursive function. In that case you do need to explicitly write out the error set.

Ah interesting approach, thx for the answer.

1. Doesn't that risk introducing accidental breaking changes by adding a new error to the set in the implementation, since the set of errors is inferred from the implementation? Having a compile error in this case in Rust is often the last barrier standing between me and an accidental major semver bump (since callers have to exhaustively match on the error conditions) 2. Can you have data in the variants of the error enumeration?

> Doesn't that risk introducing accidental breaking changes by adding a new error to the set in the implementation, since the set of errors is inferred from the implementation?

Do you have an illustrative example? (I'm not implying it's not possible, just trying to think of a good example so I can give a good answer)

> since callers have to exhaustively match on the error conditions

If it's exhaustiveness that you're referring to, zig will make you handle all the possible errors (you can still do a catch all type thing when handling errors, which has the potential to "hide" an error that you otherwise wanted to handle explicitly).

> Can you have data in the variants of the error enumeration

No, they compile to just integers. Essentially it compiles to the same as C function that returns an `int` representing the error (with your actual return type passed in as a pointer, say).

Another limitation of zig error values is I think they're globally scoped, so you potentially could have two libraries have clashing error names that you then can't differentiate (I don't know if there are any plans to try and resolve that).

I will say that this automatic error set inference gives writing zig code this lovely "flow", where I do some error checks at the top of the function and early return some errors (which I just invent the names of there and then) and then move onto the happy path of the function, happy in the knowledge that the error handling is already "correct" (in that if the error isn't handled it'll (typically) bubble all the way up to main and exit the program). Any refinement on how a specific error is handled, I can go back to an appropriate place in the call stack and handle it. I always feel like it's helping me write correct code.

ErrorType!usize