August 30th, 2024

Rust's Ugly Syntax (2023)

The blog post addresses complaints about Rust's syntax, attributing them to misunderstandings of its semantics. It suggests simplifying semantics for readability while maintaining performance and safety features.

Read original articleLink Icon
Rust's Ugly Syntax (2023)

The blog post discusses the common complaints regarding Rust's syntax, suggesting that many of these complaints stem from misunderstandings of Rust's semantics rather than the syntax itself. The author presents a function that reads the contents of a binary file, illustrating its syntax and how it could be perceived as "ugly." Various alternative syntaxes from hypothetical programming languages are provided to highlight how different syntax could look. The author then explores simplifying Rust's semantics while retaining its syntax, ultimately proposing a version of the function that is less complex and more readable. This involves removing nested functions, generic constraints, and excessive error handling, which could lead to a more straightforward implementation. The post concludes that while Rust's syntax may seem cumbersome, it serves specific performance and safety purposes that are integral to the language's design.

- Many complaints about Rust's syntax are actually related to its semantics.

- The author provides examples of how Rust's syntax could be simplified while maintaining functionality.

- Simplifying Rust's semantics can lead to more readable code, but may sacrifice some performance benefits.

- The discussion emphasizes the balance between syntax complexity and the language's safety and performance features.

Link Icon 21 comments
By @sedatk - 5 months
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.
By @wiz21c - 5 months
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...

By @tmtvl - 5 months
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))))
By @MetricExpansion - 5 months
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")
      }
  }
By @jiwangcdi - 5 months
> 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.

By @eterps - 5 months
Just give me Rattlesnake or CrabML and I'll stop complaining :-)
By @anonymous2024 - 5 months
I wonder. How does Rust syntax compares with https://www.hylo-lang.org/ syntax? That also is memory safe, typesafe, and data-race-free.
By @librasteve - 5 months
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]
By @qalmakka - 5 months
People that complain about Rust's syntax never have never seen C++ at its worst
By @tevelee - 5 months
The article just turned Rust into Swift. Nicer syntax, same semantics
By @apatheticonion - 5 months
Someone needs to tell them about async Rust. Big yikes.
By @mgaunard - 5 months
There are several problems with the C++ variant, which could have been easily avoided by just following the original Rust more closely.
By @AxelLuktarGott - 5 months
Is it really better to remove the error case information from the type signature? Aren't we losing vital information here?
By @Woshiwuja - 5 months
So you just end up with python at the end?
By @macmac - 5 months
My hot take is that Rust should have been a Lisp. Then it could also have had readable macros.
By @mjburgess - 5 months
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())
By @remcob - 5 months
Why stop there and not go all the way to

    pub fn read(path: Path) -> Bytes {
      File::open(path).read_to_end()
    }
By @singularity2001 - 5 months

   "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
By @oguz-ismail - 5 months
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.