top | item 41397679

Rust's Ugly Syntax (2023)

161 points| nequo | 1 year ago |matklad.github.io

165 comments

order

sedatk|1 year ago

I think the article makes a good point, but the actual example isn’t Rust’s worst, not even close. It gets really hard to follow code when multiple generic types are combined with lifetime markers. Then it truly becomes a mess.

LoganDark|1 year ago

I always, always forget what `'a: 'b` means, because I remember it always being the opposite of what I think it is, but memorizing that obviously doesn't work because then it will just flip again the next time. It's so annoying.

namjh|1 year ago

IMHO the mentioned examples of complexity like multiple type variables and lifetimes with bounds are for who "really" wants compile-time contracts. These are mostly opt-in so higher level use cases(like writing backend business logics) should not care about that, just wrapping everything with Boxes and Arcs.

Of course Rust is not perfect; there is some 'leakages' of low level aspects to high level like async caveats(recursion, pinning, etc.). I'm not sure how these can be avoided. Maybe just trial-and-errors for all..?

yulaow|1 year ago

throw in some async too and I really lose myself most of the times

nazka|1 year ago

That's why I am a huge fan of Rust but at the same time at the end of the day all I want is the features of the language that Rust has minus the memory management and a GC. That would be my dream language. If only ReasonML/Rescript were more popular... Or I guess Elixir

hckr1292|1 year ago

Agree about the example! I can't tell if this article is tongue-in-cheek or earnest. I'm unclear on the point the author is trying to make.

wiz21c|1 year ago

For my own situation, the articles present the right way to express all possible performance/error handling (which is expected in a standard lib) and then goes on to show how I actually code it in my own softawre where I don't really need the level of detail/finetuning of the standard lib.

Interestingly, my life starts at the end of the article, with the simple verison of the code, and as my understanding of rust widens, I go up to the beginning of the article and better define my function...

awesomebytes|1 year ago

I've only learned a tiny bit of Rust, and I feel the same. Going from the bottom up, makes it all make so much sense. (Albeit I still like the Rattlesnake syntax haha)

tmtvl|1 year ago

Aw, no Rasp variant? Let's brainstorm it up...

  (defun read (path)
    (declare (generic P (AsRef Path))
             (type P path)
             (returns (io:Result (Vector U8))))
    (flet ((inner (path)
             (declare (type (Ref Path) p)
                      (returns (io:Result (Vector U8))))
             (try-let ((file (File:open path))
                       (bytes (vector)))
               (declare (mutable file bytes))
               (try (read-to-end file bytes)
                    (Ok bytes)))))
      (inner (as-ref path))))

MetricExpansion|1 year ago

If I understood all the semantic properties, including the separate compilation requirements, correctly, here’s how I think it would be done in Swift with the proposed nonescapable types features (needed to safely express the AsRef concept here). (Note that this doesn’t quite compile today and the syntax for nonescaping types is still a proposal.)

  @usableFromInline
  func _read(pathView: PathView) throws(IOError) -> [UInt8] {
      var file = try File(pathView)
      var bytes: [UInt8] = []
      try file.readToEnd(into: &bytes)
      return bytes
  }
  
  @inlinable
  public func read<Path>(path: borrowing Path) throws(IOError) -> [UInt8] where Path: PathViewable, Path: ~Copyable {
      try _read(pathView: path.view())
  }
  
  // Definitions...
  
  public enum IOError: Error {}
  
  public protocol PathViewable: ~Copyable {
      func view() -> PathView
  }
  
  public struct PathView: ~Escapable {}
  
  public struct File: ~Copyable {
      public init(_ pathView: borrowing PathView) throws(IOError) {
          fatalError("unimplemented")
      }
  
      public mutating func readToEnd(into buffer: inout [UInt8]) throws(IOError) {
          fatalError("unimplemented")
      }
  }

jiwangcdi|1 year ago

> The next noisy element is the <P: AsRef<Path>> constraint. It is needed because Rust loves exposing physical layout of bytes in memory as an interface, specifically for cases where that brings performance. In particular, the meaning of Path is not that it is some abstract representation of a file path, but that it is just literally a bunch of contiguous bytes in memory.

I can't understand this. Isn't this for polymorphism like what we do this:

```rust fn some_function(a: impl ToString) -> String { a.to_string(); } ```

What to do with memory layout? Thanks for any explanation.

K0nserv|1 year ago

Rust needs to know the exact size, layout, and alignment of every argument passed to a function to determine how it gets passed(register(s) or spilled to stack) and used. For example PathBuf and String can both be turned into a reference to a Path, and while they have the same size their layout and implementation of `as_ref` differ.

As for `impl`,

    fn foo(a: impl ToString)
is syntactic sugar for

    fn foo<S: ToString>(a: S)
The reason the standard library doesn't use this is because the code predates the introduction of `impl` in argument position.

The reason the function takes `AsRef<Path>` instead of `&Path` is callsite ergonomics. If it took `&Path` all callsites need to be turned into `read(path.as_ref())` or equivalent. With `AsRef<Path>` it transparently works with any type that can be turned into a `&Path` including `&Path` itself.

eterps|1 year ago

Just give me Rattlesnake or CrabML and I'll stop complaining :-)

frankie_t|1 year ago

Wanted to say the same. He straight up conjured a good looking code in CrabML as an example of similar level of "ugliness", while it has about three times less syntax noise.

stavros|1 year ago

What's Rattlesnake? I can't find anything at all online.

baq|1 year ago

Choked on my coffee when I got to Rattlesnake. Great name.

librasteve|1 year ago

Here's the cleaned up version of Rust from the OP:

  pub fn read(path: Path) -> Bytes {
    let file = File::open(path);
    let bytes = Bytes::new();
    file.read_to_end(bytes);
    bytes
  }
Here is is in raku (https://raku.org):

  sub read(Str:D $path --> Buf:D) {
    $path.IO.slurp: :bin
  }
[the `--> Buf:D` is the raku alternative to monads]

sedatk|1 year ago

Then it’s just this with C#:

  public byte[] Read(string path) => File.ReadAllBytes(path);
I think the article’s trying to explain a concept using an arbitrary piece of code from stdlib, not necessarily that specific scenario (opening and reading all bytes from a file).

qalmakka|1 year ago

People that complain about Rust's syntax never have never seen C++ at its worst

olologin|1 year ago

C++ is relatively easy to read. The only troubles I had is reading STL's sources because of all _ and __ prefixes, and understanding template errors from compiler, but that will soon be fixed with concepts.

tevelee|1 year ago

The article just turned Rust into Swift. Nicer syntax, same semantics

apatheticonion|1 year ago

Someone needs to tell them about async Rust. Big yikes.

carlmr|1 year ago

I'm a big Rust fan, but async Rust is an abomination.

mgaunard|1 year ago

There are several problems with the C++ variant, which could have been easily avoided by just following the original Rust more closely.

AxelLuktarGott|1 year ago

Is it really better to remove the error case information from the type signature? Aren't we losing vital information here?

treyd|1 year ago

The std::io error type is defined roughly as:

    type Result<T> = std::result::Result<T, io::Error>;
So it's actually fine, since we're specifying it's an IO result. This is a fairly common pattern.

Woshiwuja|1 year ago

So you just end up with python at the end?

macmac|1 year ago

My hot take is that Rust should have been a Lisp. Then it could also have had readable macros.

kzrdude|1 year ago

What if Rust was an OCaml-ish.

mjburgess|1 year ago

Kinda disingenuous, you don't reskin one language in another to make an argument about syntax -- you develop a clear syntax for a given semantics. That's what rust did not do -- it copied c++/java-ish, and that style did not support the weight.

When type signatures are so complex it makes vastly more sense to separate them out,

Consider,

  read :: AsRef(Path) -> IO.Result(Vec(U8))  

  pub fn read(path):
    inner :: &Path -> IO.Result(Vec(U8))

    fn inner(path):
      bytes := Vec.new()

      return? file := File.open(path) 
      return? file.read_to_end(&! bytes)
      return OK(bytes)
    
    inner(path.as_ref())

kelnos|1 year ago

Rust does allow you to split out generic specifications into a separate "declaration block" so things don't get too busy. Like you could write the original Rust code as:

    pub fn read<P>(path: P) -> io::Result<Vec<u8>>
    where
        P: AsRef<Path>,
    {
        // ...
    }
Personally I don't find your example with the type signature completely separate to be easier to read. Having to look in more than one place doesn't really make sense to me.

Funny, though, Rust's 'where' syntax sorta vaguely superficially reminds me of K&R pre-ANSI C:

    unsigned char *read(path)
        const char *path;
    {
        /* ... */
    }

_answ|1 year ago

I have a feeling that most of the clarity you find in your example comes from better use of whitespace. Consider:

    pub fn read<P>(path: P) -> io::Result<Vec<u8>>
    where
        P: AsRef<Path>,
    {
        fn inner(path: &Path) -> io::Result<Vec<u8>> {
            let mut bytes = Vec::new();
    
            let mut file = File::open(path)?;
            file.read_to_end(&mut bytes)?;
    
            Ok(bytes)
        }
    
        inner(path.as_ref())
    }
Plus, your example does not have the same semantics as the Rust code. You omitted generics entirely, so it would be ambiguous if you want monomorphization or dynamic dispatch. Your `bytes` and `file` variables aren't declared mutable. The `try` operator is suddenly a statement, which precludes things like `foo()?.bar()?.baz()?` (somewhat normal with `Option`/`Error`). And you weirdly turned a perfectly clear `&mut` into a cryptic `&!`.

Please don't assume that the syntax of Rust has been given no thought.

tcfhgj|1 year ago

To me this example is not more clear than normal Rust

blksv|1 year ago

I strongly support your point, but the example is still sand-in-the-eyes for me. I hold that one symbol should not alter the semantics of a program and there should never ever be sequences of one-symbol syntactic elements.

In an Ada-like language, it would be something like

  generic
      type Path_Type implements As_Path_Ref;
      type Reader implements IO.File_Reader;
  function Read(Path: Path_Type) return Reader.Result_Vector_Type|Reader.Error_Type is
      function Inner_Read(P: Path) return Read'Result_Type is
      begin
          File:  mutable auto := try IO.Open_File(P);
          Bytes: mutable auto := Reader.Result_Vector_Type.Create();
          try Reader.Read_To_End(File, in out Bytes);
          return Bytes;
      end;
  begin
      return Inner_Read(Path.As_Ref());
  end Read;

degurechaff|1 year ago

Rust target user is C/C++ developer. not using brace is out of options.

demurgos|1 year ago

People may disagree on specifics, but you're definitely right that being able to separate the function signature from its definition would be very helpful in complex cases.

timeon|1 year ago

Now there are too many colons.

remcob|1 year ago

Why stop there and not go all the way to

    pub fn read(path: Path) -> Bytes {
      File::open(path).read_to_end()
    }

oneshtein|1 year ago

How to return an error in your example?

singularity2001|1 year ago

   "I think that most of the time when people think they have an issue with Rust’s syntax, they actually object to Rust’s semantics."
You think wrong. Rust syntax is horrible because it is verbose and full of sigils

Woshiwuja|1 year ago

Still trying to understand when i have to put ||

oguz-ismail|1 year ago

The final version is still ugly. Why `pub fn'? Why is public not the default and why do you have to specify that it's a function? Why `: type' and `-> type', why can't type go before the identifier? Why do you need `File::' and `Bytes::'? What is that question mark? Why does the last statement not need a semicolon? It's like the opposite of everything people are used to.

kelnos|1 year ago

> Why `pub fn'?

I prefer it this way; defaults should be conservative or more common: I write many many more private functions than public. I'm not sure what your objection to 'fn' is... seems like a superficial problem. It likely makes the language easier for the compiler to parse, and to me, it makes it easier to read.

> Why `: type' and `-> type', why can't type go before the identifier?

Because putting the type afterward is more ergonomic. If you're used to C/C++/Java/etc. it feels weird, but once you start writing code with the type after the declaration, it feels much more natural.

> Why do you need `File::' and `Bytes::'?

I'm not sure what you mean here. They're types. You have to specify types.

> What is that question mark?

The question mark is basically "if the Result is Ok, unwrap it; if it's an Err, return it immediately."

> Why does the last statement not need a semicolon?

Leaving off the semicolon returns the last expression from the block.

> It's like the opposite of everything people are used to.

Maybe if your experience with programming languages is fairly limited...

pta2002|1 year ago

Short answer for the type ordering and `fn`: because C/C++/Java tried that type of syntax and the result was an ambiguous grammar that is way too hard to parse, not to mention C's overly complicated pointer syntax.

atoav|1 year ago

As someone who doesn't think it is pretty, but knows Rust I went through all your points and let me assure you except for the one where you wonder why the syntax can't be more like C/C++ where it comes down to taste, all of your questions have an answer that really makes sense if you understand the language.

E.g. making pub default is precisely the decision a language would make that values concise code over what the code actually does.

uasi|1 year ago

Your points have nothing to do with ugliness.

> Why `pub fn'? Why is public not the default and why do you have to specify that it's a function?

If public were the default, you'd end up having to make other functions `priv fn` instead.

> Why `: type' and `-> type', why can't type go before the identifier?

It's easier to parse, and most major typed languages other than C/C++/C#/Java put the type after the identifier.

> Why do you need `File::' and `Bytes::'?

Seriously?

> What is that question mark?

The final version doesn't use a question mark.

> Why does the last statement not need a semicolon?

This is a legitimate question. In Rust, the last statement without a semicolon becomes the return value.