Hacker News new | past | comments | ask | show | jobs | submit login

As a side note, looking at this block of code, the syntax is just so ugly and overcrowded. I don't get how a relatively new language ended up looking so messy so fast; it's like no one cared about readability.



Partly personal preference in both what is readable and in how much you optimize caller convenience vs simple callee code. I think it's fine to just expect the caller to do the conversion. In fact, it's often automatic. (Eg if you have a PathBuf, then just sticking a & in front of it is sufficient to supply &Path via Deref<Target = Path>.) So I'd write this function as follows:

    fn read(path: &Path) -> io::Result<Vec<u8>> {
        let mut file = File::open(path)?;
        let mut bytes = Vec::new();
        file.read_to_end(&mut bytes)?;
        Ok(bytes)
    }
which I find readable. Comparing to another language...I haven't written Go in a while, but I think the rough equivalent would be:

    func Read(path *Path) ([]byte, error) {
        file, err := os.Open(path)
        if err != nil {
            return nil, err
        }
        slice, err := io.ReadAll(file)
        if err != nil {
            return nil, err
        }
        return slice, nil
    }
(Maybe in this case you'd just return the io.ReadAll rather than doing an if, but I'm trying to use similar constructs. The ? is something you can use all throughout the function, not just at the end, so I tried to match that pattern.)

[edited: I said there wasn't a io.ReadToEnd extracted from io.ReadFile in Go, but jatone pointed out it's called io.ReadAll.]


I never understood why Rust requires '&mut' while passing parameters.

        file.read_to_end(&mut bytes)?;
`bytes` is already mutable. `read_to_end` is already declared as taking a `&mut Vec<u8>`. So why force every caller to do a `&mut` un-necessarily ?


It's so that a reader of the calling code can immediately reason about which calls might be mutating state. If you have 10 calls, all taking &bytes, any of which may or may not be mutating the data while others are merely reading it, it becomes very difficult to reason about.

By being explicit about this, Rust becomes easier to read, but (slightly) harder to write.


Shared borrows and unique borrows also behave very differently in Rust. '&mut' for unique borrows is a bit of a misnomer, because passing some objects via a shared '&' reference might also mutate state 'indirectly' via internal mutability.


> So why force every caller to do a `&mut` un-necessarily ?

Because it's not unnecessary? `bytes` is a `Vec`, not a `&mut Vec`, those are different things, and Rust generally avoids magically doing things under the covers. So it doesn't auto-ref (let alone auto-mut-ref) parameters, only subjects.


    fun read(path: &Path): io.Result[Vec[UInt8] {
      var file = File.open(path).orReturnErr()
      var bytes = Vec.new()
      file.readToEnd(&mut bytes).orReturnErr()
      Ok(bytes)
    }
would be good enough for me, tbh.


> File.open(path).orReturnErr()

Method syntax is reserved for functions. There are no method syntax macros. Since a return in a function terminates execution of this function, and not its caller, your example would not work.


Right, so this should really be .orReturnErr , by analogy with .await . We just overload the dot symbol to mean both "method call" and "weird monadic stuff is happening here".


Exactly. ! for macros was a mistake anyway, I guess that's why they stopped using it with await.


It wasn't. Without "!" it isn't obvious that you cannot take a:

  let a: Result<u8, u8> = /* whatever *;
  let b = match a {
      Ok(v) => Ok(foo(v)),
      e => e,
  };
and transform it into:

  let a: Result<u8, u8> = /* whatever *;
  let b = a.map(foo);
if "foo" is a macro. When you substitute "foo!" for all the "foo" above, it is obvious.


Perhaps macros shouldn't do things that aren't "obvious" then?


As a rule, rust rejects the "simpler" choice that instead lay the burden of correctness at the feet of the user. I'm glad they keep a hard line here.


> I guess that's why they stopped using it with await

await cannot be a macro, it has to be built into the language, and so it doesn't have the ! because it's not one.


You realize the syntax has changed more than that?


Yes, but "->" vs ":", or kinds of brackets are superficial, while method syntax has a specific meaning.


These differences are mostly superficial. We all need to decide if we're going to avoid a language because its punctuation choices aren't "good enough" for us or if we're going to choose the best tools for building useful systems.

"orReturnErr()" seems quite against the Rust philosophy though — refusing the method call syntax for early return means folks can easily miss something important. Rust uses ? for error handling, ! for macros, and keywords (with no parenthesis) for other control flow. I prefer it that way.


I don't know Rust but converting that code snippet of Go code you wrote to (non-idiomatic [wouldn't use upper-case characters, namespaces used doesn't actually exists]) Clojure:

    (defn Read [path]
      (io/ReadToEnd (os/Open path)))
Now that's what I call readable!


You can chain calls in Rust. If read_to_end were written to allocate and return a fresh Vec, you could do this:

    File::open(path)?.read_to_end()
but instead read_to_end is written to take a presupplied Vec so the caller has the option to reduce allocations.


> Now that's what I call readable!

Unsurprisingly APIs can be more readable when you don't ponder why they're designed the way they are and completely ignore their purpose.

The `read_to_end` API[0] was designed such that the buffer could be reused in order to avoid unnecessary allocations.

There are helpers for common tasks, though people may not know or notice them. In the case of `Read::read_to_end`, it's called std::fs::read[1], which actually preallocates the vector based on file size internally so might be faster than hand-rolling `read_to_end`.

So here's your code in Rust, for the last 3 years or so (since 1.26 was released):

    fn read<P: AsRef<Path>>(path: P) -> io::Result<Vec<u8>> {
        std::fs::read(path)
    }
[0] and all of the Read trait in general

[1] https://doc.rust-lang.org/std/fs/fn.read.html


Clojure is also a much higher-level language than Rust (and arguably Go too)


bytes, err := ioutil.ReadAll("path.txt")

is the golang equivalent.


To be clear you can write

    let bytes = std::fs::read(“path.txt”)?;
If you’re trying to actually do this in real code. The example is how you’d write this function yourself, but if you’re just trying to Get It Done, it’s significantly easier than the example.


The syntax is ugly and can become overcrowded, but is in the HIGH side of readable.

Compare:

    def read(path):
What is path? Unknow. What it read? Unknow. What it return? Unknow

    pub fn read<P: AsRef<Path>>(path: P) -> io::Result<Vec<u8>> {
What is path? Anything that can become path on the file system.

What it read? Anything that can be pointed with that PATH

What it return? Vec of bytes or a IO::Error

PLUS: This code is compiled tot he exact types that call it, Path not allocate, the result can be cloned, the errors are defined in IO, ....

--

Rust is very readable: Almost any line tell you what is happening and what expect.

Where it get messy is when it hide that crucial info. But is not many times it happens.


If you're talking about a language being readable to those that aren't proficient, Rust is about as bad as one could imagine.

Your example is also less about Rust and more about type signatures.


> Your example is also less about Rust and more about type signatures.

No, is HOW Rust use type signatures. Rust is in the camp of "advanced usage", not like C#, Python and others, where a "type" is barely for classification. Instead, Rust use types to inform: Memory type (heap vs stack), Cost (cheap to copy or must clone and can be potentially very big), Permisions (read/mutable), Behavior, Share-ability (can go in threads), lazy or not (iterable), etc.

It use types for a lot, and communicate much information densely.

P.D: Not always perfect and I agree that the syntax is not as nice to me, but is incredible how much you can understand from most codebases just reading, even on docs (not need IDE assistance that much)


> No, is HOW Rust use type signatures.

No, I don't really agree. The example was "def read(path)", which is intentionally vague and any typed language would clarify.

Your points can be/are valid, but they don't really change the fact that the above example was a cherry pick, and Rust's (over)use of symbols and special characters make it, again, about as hard to read as one could imagine. Just because you _can_ derive meaning from it doesn't change that.


Ok, so how you do it or what can be used as counter-example?


func Read (p Path) ([]byte, error)

From your post:

> What is path? Anything that can become path on the file system.

> What it read? Anything that can be pointed with that PATH

> What it return? Vec of bytes or a IO::Error


With io::error and is set!


Right...so it's about types, not Rust.


I think that types are definitely useful in one sense, but it's not all goodies. Things like `collect` end up in return-type polymorphism teritory that require you to grok the context in a sort of deeper way than, say, with random Java code.


Yeah, the other side of the coin is idioms (like things that people understand as "pythonic" or idiomatic).

Certain idioms not make much sense at first, but a big part of be on Rust is learn them. Eventually, most of it make sense and are very predictable (is incredible how much adherence exist for them in the whole community!).


Come on, it is definitely not the case that nobody cared about readability. My sleepless nights after arguments about syntax in 2011 and 2012 very much say otherwise.


Or the endless arguments about await, or that time I cried because the discussion over what we call integers was so contentious. We all cared quite a bit about syntax. Heck that’s partially why almost none of it is novel!


I don’t think that when people say “Rust has ugly syntax” they mean that Rust has ugly syntax. It seems that what they are complaining about is actually Rust semantics, which needs extra syntax to express it.

Blog post idea: express this snippet (with references, aliasing control, generics and all) in pseudo C++, Java, Python, etc. Then express “reading a file in gc language with exceptions” in pseudo Rust.


To take a guess at what you don't like, it can be equivalently spaced out a bit more like:

    pub fn read<P>(path: P) -> io::Result<Vec<u8>>
    where 
        P: AsRef<Path>,
    {
And of course if you were using a lot of u8 vec results, you might create a shorter alias for that.


I think rust syntax is more readable and self explains. Having syntax more minimal, more use of symbols, spaces and tab make them beautiful but they are really hard to get if it is written by someone else. I personally noticed it's more easy understand other rust project than other langs project.


I find the syntax very readable. What's your problem with it?


I imagine it's that there's a fairly high ratio if function signatures to other lines of code, and function signatures are particularly gnarly in rust compared to many other languages, at least when lifetimes are involved.


I wonder if this is a side-effect of rust function signatures being very precise, as in the function is very clear about the allowable type and ownership of its parameters and return values, where most languages can't even express things at that level of precision so people's eyes glaze over when reading it.


I imagine so. A function signature is rust encodes more information than a lot of other languages. From my (very) limited experience, without lifetimes they look fairly similar to C++, and then adding lifetimes just makes them busier and more complex, but that's because you're encoding additional information.


I agree that I find lifetimes a bit hard to read, but outside of that I usually find Rust's syntax really clear for an algol-like. For example, they use the ML-style type declaration variable: type, which I find more readable than the C-like type variable. There's a different syntax for namespace and function chaining, which I also like.


Compare to Julia

    # Your big function has specific types
    function my_function(x::UInt, y::UInt)
        [ ... ]
    end

    # Wrapper function to minimize monomorph. cost
    my_function(x::Integer, y::Integer) = my_function(UInt(x), UInt(y))


Rust need a lot of extra stuff in function signatures to do its magic. Julia couldn't possibly do what Rust does because it doesn't have enough information


No. They didn't have to have a pub keyword, which takes space. They didn't have to have the ugly syntax to make functions generic: Julia's functions are generic without using those brackets. Rust didn't have to have explicitly needed return types, or having to have those fully specified.


So how do you make the difference between public and private code without a pub keyword? And for the "explicit return types", I think it makes the code more readable. You seem to think that what is your personal preference (something that looks like Julia) is best practice. It's not, it's just a preference.


Both Julia and Python has a concept of public and private code, neither have a pub keyword.

I don't believe Julia or Python is "best practice" over Rust. I even empathize with the opinion that explicitness may be preferential sometimes. But you _must_ be able to see how Rust's syntax overload obfuscates the core business logic, right?


The difference between pub in Rust and _/__ in Python isn't that big. It's like Go that uses a capital letter to say what code is public. There's also a difference in philosophy between public by default and private by default. All in all, it's a small detail and doesn't affect things.

Sure, business logic is more readable in something like OCaml than in Rust. On the other hand, Rust isn't really worse than something like Java, and I like having more information than what you have in Python. I'll also add that Rust is often chosen for speed and reliability, and that part of that syntax is here to guarantee that. It's not directly business logic in the traditional sense, but it's something you expect from your code.

All in all, Rust is still not at the level of the "platonic ideal for syntaxes", especially not for most of the code I write (business logic for web apps, which wouldn't need Rust's speed). Still, in the realms of programming languages that are used, it's one of the best.


> The difference between pub in Rust and _/__ in Python isn't that big.

FWIW `__` doesn't mean "this is part of the implementation", it means "I'm designing a class for inheritance and I don't want child classes to stop on my attributes".


> Both Julia and Python has a concept of public and private code, neither have a pub keyword.

No. Python certainly does not have a concept of private code[0]. The Python community has a suggestion of things being implementation details.

Surely you can see that absolutely would not be good enough for a language which strives for static correctness? And even more so as the soundness of `unsafe` blocks often relies on the invariants of the type it's used in?

[0] well that's not entirely true, I guess there's native code.


Fully specified function signatures are a conscious decision because it allows you to type check and borrow check a piece of code without needing to analyze the bodies of other functions. I also love it as a human because there is no guesswork involved.


Those are all trade-offs. Ditching fully-specified function signatures makes type inference more difficult and results in poorer type inference errors


Rust generics can be written in a more compact way,

    fn f(x: impl Test) { ... }
Is basically the same as

    fn f<T: Test>(x: T) { ... }
Having to annotate the return value of functions is partly to make sure don't break semver accidentally, but it also constrains type inference to a local search rather than global search, avoiding spooky action at distance (when modifying a function changes the signature of another function). It seems that semver hazards are more important in languages with static types.

That pub? It's punching waaay above its weight! Having things private by default protects against semver hazards too, but it's also the major tool that enable writing safe code that uses unsafe { .. } blocks underneath. That's because a single unsafe block typically taints the whole module it's contained (such that even safe code needs a safety review). Being able to write safe abstractions of unsafe code is the pull of Rust so people need to get it right.

For example, setting the field len of a Vec to a value larger than what's initialized in its buffer will cause UB when accessing that memory; if this field weren't private, making a safe Vec would be impossible (likewise, a function that could set the len to any value must be private, etc).

So pub can only be used for things that are safe to export, and people should be careful with that.

Here's my least favorite syntax from there: that damn Ok(..). Ok-wrapping sucks. There were proposals to remove it, but they were rejected. There's a library that removes it [0], and people should use it. It also makes the Result<..> return type implicit in the function signature too.

[0] https://boats.gitlab.io/blog/post/failure-to-fehler/#but-abo...


Your comparaison isn't only about syntax, it's also about the presence or absence of static types.


Those two are related, clearly. It's not that Rust has added words that literally do nothing. Instead, the designers of Rust packed all kinds of things into the syntax that other languages decided would cause too much noise.

  * The output type must be explicitly named instead of inferred
  * The let keyword for creation of variables instead of merely assignment
  * The mut keyword to distinguish between mutable/immutable variables
  * The pub keyword, instead of another mechanism
  * Semicolons needed after every expression
And more. Again, for every one of them, you can argue that it's _good_ for it to be there, because it serves a function. But you end up with a language with a hugely bloated syntax where the business logic is sort of swamped in these accessory keywords.


> The output type must be explicitly named instead of inferred

That isn't syntax. It's a decision about analysis.

> The pub keyword, instead of another mechanism

The "other mechanisms" discussed are in fact not providing this feature, but pretending you needn't care, and I'd argue Rust shows that in fact you should care and pretending otherwise is a problem.

> Semicolons needed after every expression

On the contrary, semicolons are at the end of Rust's statements. If what you've got is an expression you don't need a semicolon. For example here's how log is defined on an f64 (a double precision floating point number)

    pub fn log(self, base: f64) -> f64 {
        self.ln() / base.ln()
    }
Notice there's no semicolon there because that function is an expression, we want the value of this division as the result of the function.


I don't agree that the syntax is "hugely bloated". The return type is part of the business logic. mut and pub are here to make some things explicit, so again I don't mind them. Rust is one of those languages that wants to make more things explicit because the people using it need those things to be explicit.


The worst decision regarding Rust syntax was following C++ syntax. Due to this, we write, in type syntax

Things<Like<This, That>, That> -- as in C++

Instead of a more legible (and very familiar to Rust authors)

Things (Like This That) That -- as in Haskell

Inherited from C++, we had also :: for namespacing (while a dot . could be perfectly be used for that), and tons of other stuff like <> for scoping generics.

Without some of that we could have:

    pub fn read<P: AsRef Path>(path: P) -> io.Result (Vec u8) {
      fn inner(path: &Path) -> io.Result (Vec u8) {
        let mut file = File.open(path)?;
        let mut bytes = Vec.new();
        file.read_to_end(&mut bytes)?;
        Ok(bytes)
      }
      inner(path.as_ref())
    }
Perhaps it looks a little better? It looks for me, at least.

For doing better than that we could drop C-like syntax perhaps?

The trouble is that languages have a weirdness budget - there's a finite amount of novel stuff a language can have until it becomes too weird for mainstream programmers. So language designers have to pick their weird features carefully. Spending it all in novel syntax is unwise.

I think that at the time the Rust pull was safety for low level code and, as Rust was targeted to replace some C++ components, following C++ syntax was seen as a good idea.


What’s the worst decision for you is my favorite feature of the language syntax for me. I find it easy to read and very easy to navigate in the text editor because types are enclosed by balanced operators that editors can easily highlight and jump between.


> Rust was targeted to replace some C++ components, following C++ syntax was seen as a good idea.

And that was a good decision. If it were too far from the C++ syntax, I would not have learned it, because as a mostly C++ guy at the time, some similarities with C++ helped me keep interest in Rust. Also, I was unable to parse Haskell's "type parameters as function applications" syntax, which was purely confusing. The Rust syntax is an abomination, that's true, but it is a necessary evil.


The parenthesis aren't to be parsed like usual functions f(x), I think that's was the major source of confusion

It's more like this,

Thing<A> becomes Thing A

Thing<A, B> becomes Thing A B

Thing<A, B, C> becomes Thing A B C

The parens only appears when you want to further nest types

And.. okay, I reckon that the Haskell syntax for types would be unfamiliar. But, at least, it's not as cluttered


By comparison, near-source-compatibility with C is arguably the "worst" design decision that C++ made, a constant source of complexity and frustration, and yet the #1 reason behind C++'s early growth and continued success. There are a lot of different factors at play here.


Dot syntax is used when accessing a function/property on a value, :: is used when accessing a function/property on a module or type. I think it's great having this visual distinction, especially given that you can call the same function in either style:

  let x = Foo::new();

  x.bar();
  Foo::bar(x);


I thought the distinction was meaningful until I worked in a language that used dot pervasively, and saw that it was totally fine.


Function application and type annotation both using parens is pretty jarring for a lot of people. I much preferred the proposal that would have used [] like typescript or scala.


I like how the namespace and the function chaining have different characters. I think it makes it easier to read.


Surely mixing usage of space and parentheses for function application is a bad idea?


But I'm not talking about function application - I'm talking about type application.

The syntax of type application and function application is already different in rust: F<A, B> vs f(a, b).

Type application even has default and named parameters! F<X = A, Y = B> and F<A> if B has a default.

I just think this particular type-level syntax is very noisy, specially because types in Rust tend to be very nested.


I think the language team has done a great job. Rust may look a little ugly on the surface if you're just getting into it, but I'm super thankful they opted to require functions to always spell out all type signatures. Generics can get a little overwhelming but like any other skill, it just takes a few hours/days of practice to learn how to read them.


One sad aspect is the ? operator. In the beginning, Rust thought it could get away with explicitly handled return codes, without having support for direct style/exceptional error handling. But they've ended up with that in the end. If Rust had just used checked exceptions from the start (or equivalently if they had do-notation) they wouldn't have this lingering syntactic noise. Hindsight is 20/20 though - it would have been hard to convincingly predict that Rust would eventually end up with checked-exceptions-plus-syntactic-noise as its error handling paradigm - the experiment had to be carried out to see that it would end up like this. (Now, of course, exceptions have been thoroughly vindicated, and it would be silly to design a new language with explicitly handled return codes instead of checked exceptions)


You are completely wrong about ? in my opinion. Between it, built-in load carrying tagged enums (also known as sum types) and the built-in Result and Option types, they essentially solve the exception problem while still being entirely explicit. It's probably my favourite feature of Rust.


? is not "syntactic noise". Having an indication in the code of which operations might throw is very useful, and it enables a further simplification of using ordinary variant records as opposed to a special-cased "checked exception" construct.


Compare it to unsafe functions. An unsafe function can be called in an unsafe block or an unsafe function. Rust could require annotating each call to an unsafe function, even inside unsafe functions. Why don't we do that? That seems like it's also very useful for the exact same reasons.

A proper language needs to allow the programmer to choose between implicit effects and explicit effects on a case-by-case basis. The reason ? is so tragic is because Rust tried to go with explicit effects only, but found that people wanted implicit effects, and ? is the closest that Rust can get. That's why it's syntactic noise: The programmer wants to get rid of it, they don't regard it as informative for this case, but they can't get rid of it.

(And checked exceptions can be perfectly well implemented with regular data, you don't need any special cases - in fact, you can do it with the same infrastructure that's already been added in a special-cased form for async/await support...)


Didn't downvote you because I presume you are arguing in good faith, but I have very different opinion about the ? operator, it is one of the best syntactic things in Rust. In my experience, many Rustaceans agree. It's a minimal nuisance, while still being explicit in a good way.

Also, being able to call unsafe code inside unsafe functions is now considered a design mistake, and there is ongoing work to revert the default – requiring unsafe blocks even in unsafe functions: https://rust-lang.github.io/rfcs/2585-unsafe-block-in-unsafe...


But `foo()?"A context message for a human".bar();` is even better. Currently, we must use anyhow's `foo().context("Message")?.bar()` [1] for that.

[1]: https://docs.rs/anyhow/1.0.42/anyhow/trait.Context.html


The compiler does have a lint to force being explicit about unsafe in the way you describe. It is an allow by default lint called `unsafe_op_in_unsafe_fn`.


Do you have any constructive suggestions? It looks pretty readable to me, and I’ve never written any Rust.


Agree.


It is what usually happens when you design by committee instead of everyone having a common understanding and working towards the same goal under a single person/entity.

Not saying it's good or bad, just how Rust ended up where it is.


What Rust committee?


"Design by committee" - https://en.wikipedia.org/wiki/Design_by_committee

I disagree it's a pejorative term as Wikipedia puts it though. Design by committee is in many cases necessary or even wanted (standard bodies).




Consider applying for YC's Spring batch! Applications are open till Feb 11.

Guidelines | FAQ | Lists | API | Security | Legal | Apply to YC | Contact

Search: