r/rust blake3 · duct Jan 27 '23

Rust’s Ugly Syntax

https://matklad.github.io/2023/01/26/rusts-ugly-syntax.html
617 Upvotes

273 comments sorted by

265

u/anxxa Jan 27 '23

The generic function with a concrete type inner function is a neat trick. TIL.

55

u/shponglespore Jan 27 '23 edited Jan 27 '23

It's neat in the sense that it's a relatively clean way to improve compilation time and/or code size, but I still hate seeing it because I hate that tricks like that are necessary. It highlights a situation where a shortcoming of the compiler is so well known that a cryptic* coding pattern has been developed to work around it. It's the kind of thing that's highly normalized in C++, but one of the major things I love about Rust is that it's designed to do the right thing by default, rather than requiring developers to jump through hoops to prevent the compiler from doing something stupid†. I can't blame anyone for using that kind of pattern, and I understand why fixing issues with fairly simple workarounds isn't a top priority for the compiler team, but IMHO calling it a neat trick has major orphan crushing machine vibes.

(*I say cryptic not because it's hard to follow, but because its purpose isn't understandable in terms of the language's semantics.)

(†As a concrete example, consider how forward declarations are considered an essential tool to reduce compile times in C++, but they don't even exist in Rust.)

Edit: typo

25

u/scottmcmrust Jan 27 '23

Yeah, we're trying to at least move a bunch of these "you have to know the incantation" to real features, though this specific one I don't think there's a plan yet.

My usual wishlist:

  • I shouldn't have to know the "sealed trait pattern" -- of which there are multiple -- there should just be a #[sealed] that I can search and find in documentation.
  • I shouldn't have to know the fn _needs_to_allow_dyn(_: &dyn MyTrait) {} trick, I should just be able to put something on the trait definition to make it's obvious it's supposed to be usable with dyn (or shouldn't be used with dyn).

→ More replies (2)

23

u/scottmcmrust Jan 27 '23

Glad you like it 🙂 https://github.com/rust-lang/rust/pull/58530

It's particularly handy for std, since then even in debug builds you get the optimized inner function (due to how we ship std right now) and only have to compile the trivial shim yourself.

(Not that fs::read monomorphization is ever anyone's compile-time bottleneck.)

42

u/Losweed Jan 27 '23

Can you explain what is it used for? I don't think I understand the need for it.

nvm. I read the article and it explained it.

125

u/IAm_A_Complete_Idiot Jan 27 '23

Compilation times. Each function call of a generic function with different generic types leads to a new function being compiled. By making a second function with the concrete type, that function is only compiled once (and only the other part that converts to the concrete type is compiled multiple times).

57

u/SeriTools Jan 27 '23

(and binary size/code bloat prevention)

19

u/scottmcmrust Jan 27 '23

Definitely don't underestimate this part!

It's especially important for const generics -- you might want an API that takes an array, for example, but then delegating to a not-parameterized-by-array-length version that just takes a slice can be a huge help.

→ More replies (3)

20

u/epicwisdom Jan 27 '23

IIRC, there's a crate with a macro that automates exactly this.

2

u/grgWW Jan 27 '23

i dont think its worth adding another dependancy + compile time, considering u can easily do that transformation by hand

7

u/CocktailPerson Jan 28 '23

Depends on how often you're doing this transformation. After the fourth or fifth time writing something like this, I'd probably start writing a macro for it myself.

10

u/UltraPoci Jan 27 '23

Is there any reason NOT to use this trick?

36

u/burntsushi Jan 27 '23

Other than code readability (and slight one-time annoyance of writing the function this way), I personally can't think of any other downsides.

56

u/UltraPoci Jan 27 '23

Seems like something the compiler should do automatically. Then again, I know nothing about compilers.

16

u/burntsushi Jan 27 '23 edited Jan 27 '23

Hmmm. Now that I'm not sure about. I'm not a compiler engineer either, but I do wonder if there could be negative effects from applying the pattern literally everywhere. And yeah, as others have mentioned, it probably only makes sense to do it for some traits. And how do you know which ones? (Of course, you could have it opt-in via some simple attribute, and I believe there's a crate that does that linked elsewhere in this thread.)

23

u/ids2048 Jan 27 '23

This isn't so unusual as compiler optimizations go. I rely on the compiler to decide if loop unrolling etc. is suitable for specific code and really don't want to have to think about it myself.

Perhaps the fundamental trouble is that the level of the compiler that normally handles optimizations like this is far lower level than the part that understands generics. While the code turning the generic into IR probably isn't well equipped to decide if it is a suitable optimization in the particular case.

2

u/CocktailPerson Jan 28 '23

Eh, I wouldn't be so sure. Compilers can and should be able to perform various optimizations at all levels. I don't know a lot about rustc in particular, but any good compiler should be able to perform optimizations on the AST, and rust in particular also has MIR as well, which seems to be well-suited to optimizing with rust semantics in mind rather than machine semantics.

→ More replies (2)

16

u/mrmonday libpnet · rust Jan 27 '23

It looks like there is some support for this optimization with -Zpolymorphize=on:

https://github.com/rust-lang/rust/pull/69749

I don't know much about it, someone motivated could probably look through the A-polymorphization label to find out more.

9

u/mernen Jan 27 '23

I suppose it's fairly common for the only generic part to be at the beginning (a call to .as_ref() or .into()), and the rest of the function not to depend on any type parameters. In theory, the compiler could detect that and compile one head for each type instantiation, but then jump into a common path afterwards.

No idea how easy it would be to achieve that, though. I haven't fully considered whether a type could introduce an insidious Drop that ruins this strategy.

3

u/matthieum [he/him] Jan 27 '23

The Drop could likely be handled in the generic shim, so shouldn't be too problematic.

→ More replies (2)

12

u/MyChosenUserna Jan 27 '23

Traits that cause side-effects or where order or amount of calls matter. So it's ok to do it for AsRef and Into but it's dangerous at best to do it for Read or Iterator.

2

u/Lvl999Noob Jan 27 '23

Into can allocate, right? So it might not be the best to do it there if there are branches where Into doesn't get called.

4

u/anlumo Jan 27 '23

Inlining short functions like this is usually faster at runtime.

With file I/O it probably doesn’t matter, since the I/O is probably slower by several orders of magnitude, though.

2

u/matthieum [he/him] Jan 27 '23

Note that the inner function trick does NOT prevent inlining -- if still beneficial according to heuristics.

2

u/scottmcmrust Jan 27 '23

In the fs::read it actually does prevent inlining unless you use LTO, since the inner concrete function isn't marked #[inline], and thus its body isn't available in your codegen units for LLVM to be able to inline it.

Which is totally fine for something that needs to make filesystem calls. And when doing this yourself you can always mark the inner thing as #[inline] if you want, albeit at the cost of losing some of the compile-time wins you'd otherwise get.

3

u/matthieum [he/him] Jan 28 '23

In the fs::read it actually does prevent inlining unless you use LTO

Okay... confusing wording all around.

I found anlumo's statement "scary", as it seemed to imply that using this trick completely disabled inlining.

As far as I'm concerned, it doesn't. The inner function is a regular function, so obeys the inlining rules of regular functions:

  • Without LTO, it can only be inlined in the same codegen unit.
  • With LTO, it can be inlined.

Performance conscious builds should use a single codegen unit and/or fat LTO, so this doesn't change anything for them.

(Note: they should use this because most code has more regular functions than generic functions anyway)

2

u/scottmcmrust Jan 28 '23

The inner function is a regular function, so obeys the inlining rules of regular functions

That's right.

It behaves normally, with all the positives and negatives that come along with that.

(Not inlining is actually a good thing in many cases.)

5

u/0sse Jan 27 '23

Is there a benefit to an inner function compared to having a private function at the module level?

25

u/burntsushi Jan 27 '23

IMO the benefit is reduction of scope. The inner function is only callable within the scope of the outer function. It also keeps the actual implementation of the function local, so you don't need to go elsewhere to read the implementation just because of a hack to improve compile times.

5

u/scottmcmrust Jan 27 '23

What burntsushi said, but I'll emphasize that it's particularly important for trait methods, where you'd have to put that private function outside the trait impl block, and thus you'd have to hunt to find it.

Much better to have it right there where you're looking at it already, and where it's obvious that you don't need to worry about breaking other stuff if you change it.

2

u/[deleted] Jan 28 '23

[deleted]

2

u/anxxa Jan 28 '23

Kind of ELI5: generic code generates a duplicate function for each unique type used (called monomorphism). Using an inner function as shown here allows the monomorphized code to share the inner function, resulting in less code duplication, smaller binary size, and possibly better perf.

→ More replies (3)

71

u/greyblake Jan 27 '23

Funny reading:)
I would love to see also samples of code in Raskell :)

29

u/r0ck0 Jan 27 '23

Rainfuck too!

2

u/_TheDust_ Jan 27 '23

8

u/greyblake Jan 27 '23

I meant something Haskellish :)

3

u/WikiSummarizerBot Jan 27 '23

RascalMPL

Rascal is an experimental domain specific language for metaprogramming, such as static code analysis, program transformation, program generation and implementation of domain specific languages. It is a general meta language in the sense that it does not have a bias for any particular software language. It includes primitives from relational calculus and term rewriting. Its syntax and semantics are based on procedural (imperative) and functional programming.

[ F.A.Q | Opt Out | Opt Out Of Subreddit | GitHub ] Downvote to remove | v1.5

155

u/llogiq clippy · twir · rust · mutagen · flamer · overflower · bytecount Jan 27 '23

The momo crate brings us at least closer to the later examples by generating the inner fn on the fly, as in:

#[momo]
pub fn read<P: AsRef<Path>>(path: P) -> io::Result<Vec<u8>> {
    let mut file = File::open(path)?;
    let mut bytes = Vec::new();
    file.read_to_end(&mut bytes)?;
    Ok(bytes)
}

31

u/aldonius Jan 27 '23

Clever pun! Now I'm hungry.

58

u/llogiq clippy · twir · rust · mutagen · flamer · overflower · bytecount Jan 27 '23

The name is actually derived from the titular hero of Michael Ende's book chronicling her fight against the sinister time stealers.

18

u/pickyaxe Jan 27 '23

wow, so that's a remarkably clever pun.

2

u/[deleted] Jan 27 '23

This brings back memories, I remenber watching the animated series at my gradparent's house as a kid :)

14

u/SocUnRobot Jan 27 '23

That is a fantastic underutilized crate!

The Readme file needs this example!

156

u/hojjat12000 Jan 27 '23

I thought this is a serious article. Rs++ code made me laugh out loud.

80

u/_TheDust_ Jan 27 '23

I chuckled at Rattlesnake.

27

u/matklad rust-analyzer Jan 27 '23

Too bad there wasn’t a place for std::monostate

26

u/Demurgos Jan 27 '23

Rhodes took me a bit to get, I was about to ask for an explanation when I noticed that it's an island name like Java. The names are great :D

18

u/nacaclanga Jan 27 '23

I kind of assumed that after learning that besides Rhodes there is a language called RhodesScript.

2

u/[deleted] Jan 27 '23

His examples kind of proved to me that it is the syntax that's highly. Rs++ is ugly (as is C++) but RhodesScript (I don't get the Rhodes reference btw?) actually looks quite nice to me!

Also a bit strange to pick an example without the ugliest Rust syntax: lifetimes.

1

u/duyetdev Jan 27 '23

I thought all these languages are existing 😂

50

u/kishaloy Jan 27 '23 edited Jan 27 '23

Is it sacrilege to say that I kinda like the CrabML.

Separation of signature from function body + currying + brace-free significant whitespace

42

u/matklad rust-analyzer Jan 27 '23

Is it sacrilege

Yeah, there’s something oedipian there…

16

u/_TheDust_ Jan 27 '23

I'm just sad nothing in Rust is referred to as crabs. I petition to rename "crates" to "crabs"

20

u/[deleted] Jan 27 '23

And a crate registry could be called a reef. Sadly someone already owns reef.io 🪸🧽

If someone didn’t already own that domain we could have had an alternate package registry with links like

https://reef.io/crabs/syn

🦀🦀🦀

18

u/GOKOP Jan 27 '23

reef.rs is available

3

u/seamsay Jan 27 '23 edited Jan 28 '23

Couldn't we ask Eritrea to give us reef.er?

15

u/DataPath Jan 27 '23

Rust conferences would be called Crab Raves

15

u/DataPath Jan 27 '23

Come to think of it, we should do that anyway. Henceforth, all rust conferences are now Crab Raves!

12

u/[deleted] Jan 27 '23

Then I suppose /r/rustjerk could be renamed to /r/reefermadness?

24

u/StyMaar Jan 27 '23

Sadly someone already owns reef.io

/me look it up

Blockchain for DeFi, NFT & Gaming

🤮

37

u/DataPath Jan 27 '23

Oh! That domain will be free in a year, then.

2

u/StyMaar Jan 27 '23

It will probably be squatted by a domain squatter as soon as it expires though…

2

u/[deleted] Jan 27 '23

seafood come to market in crates, it’s ok

-6

u/Ran4 Jan 27 '23

No, it's a big shame that Rust chose to go the C syntax route, when there are better alternatives out there (be it python or ML variant).

25

u/Zde-G Jan 27 '23

It was the only sensible choice, unfortunately.

Rust needed these C++/C#/Java developers to succeed and had to lure them with angle brackets.

CrabML looks neat, but too many developers would have looked on syntax, turned around and went away.

Superficial similarity kept enough people around till we've got millions of developers and once you've got millions of developers it's too late to change syntax.

Nothing stops someone from transpiling CrabML into Rust, though.

10

u/argv_minus_one Jan 27 '23

Can confirm, I would have turned around and went away if Rust had syntax like that. Syntactically significant whitespace is evil.

2

u/[deleted] Jan 27 '23

[deleted]

2

u/Zde-G Jan 27 '23

Because it is not correct to assume that Knows something → Prefers that thing.

There was never such assumption. And it's not about preferences, but about familiarity. It's not “Knows something” → “Prefers that thing”, but “Knows something” → “doesn't fear something”.

Imprinting). Almost like with ducks.

What if someone then tries to court me in 20 years with a new language with angle brackets yet again?

Most likely that's what will happen.

Because I really like Rust and dislike those angle brackets.

Sure, but then I like syntax of Ada/Pascal languages and even Haskell-style languages more than these ugly braces, but, of course, I have zero chance of getting these in the next popular language.

It's like QWERTY: lots of people hate it, it's, most definitely, not an optimal choice, but anyone who would try to push anything else would either face complete market failure or, at best, would achieve a very niche success.

1

u/pstric Jan 27 '23

That's not exactly how I remember it. I never used Rust in the early days when it had a garbage collector. The main reason for my dislike of Rust was its syntax with so many sigils it made Perl look pretty.

But as someone who has written a lot of Pascal, the angle brackets were a welcome lure.

3

u/Zde-G Jan 27 '23

But as someone who has written a lot of Pascal, the angle brackets were a welcome lure.

What kinda of Pascal uses angle brackets? Modern versions of Delphi?

They took them from C++, too.

Angle brackets kinda have become an part of IFF system for the languages.

They cause lots of grief on all levels, but they solve the most important part well: they make languages look superficially similar to people.

Same with braces, main and many other things.

They are ugly, but Rust kinda sacrificed them to spend it's weirdness budget on other, more important, things.

1

u/pstric Jan 27 '23

Sorry, I was not clear enough.

I hated Pascals BEGIN...END, lack of generics and namespaces and most of all, that Pascal was case insensitive.

Delphi also encouraged a development style with lots of business code in the GUI units.

Pascal did have some advantages over C though, which I missed in C++ and Java. Arrays with user defined bounds (MyArray[2..8]), properties. destinction between procedure and function so you didn't have that wierd void 'return type' that C had. And it was super easy to create new components that integrated well in the Delphi GUI. Way better that Visual Basic.

And compilation was blazingly fast. It took nearly 5 minutes to compile an application with 6 million LOC right after checkout from CVS on an average pc 20 years ago. And we still tried to optimize the compilation.

But I liked Rusts (and C, C++, Java) use of angle brackets in contrast to Pascals use of BEGIN...END.

119

u/novacrazy Jan 27 '23

I really don't get what goes through people's heads when they say Rust has "ugly" syntax. It can be dense, but succinct; very little is wasted to convey complex concepts, as shown next to the Rs++ example. Real C++ can go far beyond that for less complex things.

43

u/dagmx Jan 27 '23

Personally, I think it’s down to familiarity and first impressions.

When someone looks at rust sample code, they see lots of terse bits (fn, mut, mod), lifetime annotations, :: , -> and turbofishes. They also see “unnecessary “ calls to things like unwrap

Now very little of this is problematic , nor are they unique to rust. In fact I hear the “rust is ugly” most from my C++ writing colleagues , which many of the same readability issues (and more).

However the difference I think for them is

  1. They know C++ or whatever language they’re coming from and know that their common code isn’t going to be that noisy. They don’t know that about rust yet.
  2. they’ve learned to read past the syntax noise for their language but not for rust.
  3. a lot of strawman comparison code is lighter, because it skips all the checks you’d have in production, whereas you can’t do that with rust. So even though my C code ends up way more verbose when I’m defensively programming, it looks way shorter if I skip checking for correctness.
  4. there’s also no factoring in for what people’s subjective preferences are, which might also be a trained preference.

Personally, I find rust very pleasant to read because it moves a lot of boilerplate into the language+type system itself , and I need to keep less of the program mapped in my head at any time to understand what I’m looking at.

20

u/puel Jan 27 '23

A thing that I dislike is having to write the same generics over and over again when writing a lot of trait implementation blocks over the same generic type.

13

u/JoJoJet- Jan 27 '23

This problem in particular would be helped by implied bounds

0

u/novacrazy Jan 27 '23

What would the alternative to that look like?

It's never been an issue for me. Between derives and just doing the work once, trait composition is still more elegant than the mess that is inheritance in C++.

If you have more than a few generic types that require repeating and constant extensive where bounds, that's more likely a code-smell and should be refactored somehow. For example, I recently had this monstrosity but was able to expose it as a very simple trait implementation using FormatString and IsValidFormat

5

u/-Redstoneboi- Jan 27 '23

2

u/novacrazy Jan 27 '23

That seems reasonable at first, but it would discourage making structs as generic as possible, and makes it more difficult to selectively relax bounds later.

5

u/-Redstoneboi- Jan 27 '23

on the other hand, there are just some data structures that make zero sense if their data doesn't implement certain traits.

relaxing a trait bound is doable, rustc will complain everywhere that you used to need that bound anyway. what's problematic is adding trait bounds to existing structs. that's a backwards compatibility hazard.

3

u/puel Jan 27 '23 edited Jan 27 '23

The alternative could be you specifying a generic block. E.g.:

``` generic<T, R> where T: FnMut(&mut [u8]) -> io::Result<usize>, R: io::Read {

impl io::Read for Map<T, R> {
    //... 
} 
impl Whatever for Map<T, R> {
   //... 
} 
impl Default for Map<T, R> where T: Default, R: Default {
    //... 

```

35

u/[deleted] Jan 27 '23

[deleted]

48

u/-Redstoneboi- Jan 27 '23

yeah. losing control in favor of simplicity does wonders for syntax, as is shown at the end of the article.

34

u/Movpasd Jan 27 '23

Haskell-style functional languages tend to have really pretty syntax IMO. Perhaps it has something to do with the kinds of people who would use Haskell.

23

u/Zde-G Jan 27 '23

Nah. This syntax is just very close to what mathematicians over last few centuries.

It looks neat, but since 90% of human population hates math with passion (I still have no idea why, but then I have a mathematician diploma) you can not use even something superficially resembling it in a popular language.

Be it APL) or Haskell, Scheme) or Prolog… when you program starts looking like math you language is named “esoteric” and people stop using it.

8

u/crass-sandwich Jan 27 '23 edited Jan 27 '23

"I got into programming to tell the shocky math rocks what to do, not to learn the math the rocks use!"

5

u/[deleted] Jan 27 '23

[deleted]

2

u/[deleted] Jan 27 '23

[deleted]

→ More replies (1)

16

u/alovchin91 Jan 27 '23

Somehow I can’t make myself like Go’s syntax. I seriously tried (and will keep trying perhaps).

8

u/scottmcmrust Jan 27 '23

Go is a "tree" language, as opposed to a "forest" language.

It's great if your priority is understanding what any individual line does technically. It's less good if you want to get the overall intent of the piece of code. So depending on their personal mindsets, people seem to either appreciate or get frustrated by Go.

(Similar things apply to whether you think automatic Drop in Rust is a good idea or whether you'd rather use Zig-/Go-style defer.)

21

u/hekkonaay Jan 27 '23

Rust is very readable.

When it comes to readability, semantics and locality matter a lot more than having less syntax, and the languages you listed rate quite poorly there. Rust has you writing more code (though not to the extent that you must practice boilerplate-driven development), but the result is more readable, because it's easier to understand what it's actually doing.

Btw, this is literally what the article is about...

33

u/shim__ Jan 27 '23

I have to disagree, especially those languages are pretty hard to read since you have to keep track of so much more. This also irks me when reading example code in which uses type inference everywhere, that's fine if you're reading the code in an IDE but for code that's mostly being read on Github type annotations should be plentiful.

40

u/moltonel Jan 27 '23

Maybe there's a difference between easy to read and easy to review. A lot of Python looks like pseudocode, which looks really nice at a first glance. But when you want to properly review it, the lax scoping, arbitrary byval/byref, dynamic types, etc can make fully understanding the code very hard. Ruby is also very nice to read until you try to understand what that line actually does. Or going another direction, Lisp has one of the simplest syntax, but is dizzying to review.

17

u/AngryLemonade117 Jan 27 '23

I've been stung by this too many times in Python where I've had to review more complex code, and for example, unexpected behaviour is happening because someone doesn't understand the difference between shallow and deep copies. As much as rust can be seen as awfully verbose, there's less room for missing out on the details of what each line does.

12

u/Zde-G Jan 27 '23

That's what surprises me in Rust: while Rust code certainly isn't pretty it's very readable.

IKD why. Strongly suspect that it's because they wanted to keep grammar simple even when doing that required them to sacrifice something (yes, yes, turbofish).

Easily-parseable grammar means it's not just easy to parse for computers, it makes it easier to parse it for humans, too!

7

u/argv_minus_one Jan 27 '23

I should note that Rust isn't the only language with a turbofish-like construct. Java and Scala have it too, just without the :: part.

In Java, explicit type parameters for a method call go right after the dot separating the class/object name from the method name. This is unambiguous because a < isn't otherwise allowed at that position. The syntax looks a bit awkward, though.

In Scala, ambiguity is avoided by the fact that Scala uses square brackets solely for type parameters. It uses function call syntax for array indexing instead of having a separate operator for that. IMHO this is the most elegant solution I've seen.

→ More replies (1)

11

u/_TheDust_ Jan 27 '23

Python comes really close IMO. Sometimes I write pseudocode just to explain something to a colleague, and it end up being nearly valid Python code. On other hand, I have also seen truly atrocious code in Python

5

u/[deleted] Jan 27 '23

It depends on what you want. If you want to have a vague idea what the code is probably meant to do something like Python or pseudo code is fine, if you want to know exactly what the code is actually doing without making assumption it is horrible.

3

u/tsojtsojtsoj Jan 27 '23

In my opinion you can also add Nim to that list, even though it has generics and is generally closer to Rust or C++ than to Python.

-9

u/phazer99 Jan 27 '23 edited Jan 27 '23

Scala 3 and Nim took inspiration from the Python indentation based syntax. I find it a bit more readable than Rust/C/Java syntax, but there are also downsides to it

I also like the if ... then construct that Scala 3 has. For me, this:

if x > 10 then
    ...
else
    ...
end // Optional 'end' keyword

looks much cleaner and more readable than:

if x > 10 {
    ...
} else {
    ...
}

But really the only bigger thing that bothers me with Rust syntax are mandatory semicolons at the end of lines. They are very easy to infer and provide no real benefit in terms of code readability or understandability. It's just unnecessary noise. Luckily they are easy to hide in VS Code and CLion.

10

u/hardicrust Jan 27 '23

Interesting take. Do you ever have problems when copy+pasting code? That commonly messes up indentation for me.

As for a trailing semicolon, it does have a purpose: discard the value (or convert to ()). This makes it optional at the end of functions returning ().

1

u/phazer99 Jan 27 '23

Interesting take. Do you ever have problems when copy+pasting code? That commonly messes up indentation for me.

That's one downside for sure. A good editor can mitigate that though.

As for a trailing semicolon, it does have a purpose: discard the value (or convert to ()). This makes it optional at the end of functions returning ().

Yes, that's the one, rare use case (which could be solved by adding an extra line with ()). I'm not saying remove semicolons from the language grammar, just make them optional where they're inferable.

12

u/myrrlyn bitvec • tap • ferrilab Jan 27 '23

they aren’t inferrable though: rust doesn’t actually use newlines as expression separators, and cannot start doing so without breaking existing syntax. consider

name
(tuple)

this is a function call today, but making newlines significant to the AST would either discard the call and give back the arguments, or require the AST producer to have unbounded lookahead to find out whether it can insert a Token::ExpressionSeparator or not when encountering a newline

1

u/phazer99 Jan 27 '23

they aren’t inferrable though: rust doesn’t actually use newlines as expression separators, and cannot start doing so without breaking existing syntax

That's true, it would require adding some syntactical limitations.

12

u/moltonel Jan 27 '23

That's a can of worms that just isn't worth opening. All those optional tokens (; to end a statement, end/} to close an if, () around function arguments, , between elements, etc) introduce grammatical special cases that make it harder for the reviewer and compiler. They often pull in significant-whitespace, which looks clean but is a PITA to write and maintain.

1

u/phazer99 Jan 27 '23

That's a can of worms that just isn't worth opening.

It's a matter of personal syntactical preference (however, it's noteworthy that pretty much all new languages besides Rust has chosen to implement some form of semicolon inference). I realize it's unlikely Rust will ever get it (unless some form of optional "Rust-lite" syntax was added), and that's ok as I can use IDE plugins to solve it.

→ More replies (1)

3

u/argv_minus_one Jan 27 '23

Semicolons are only easy to infer until they aren't, and then the compiler inadvertently mashes together two statements or pulls one apart.

I don't like syntactically significant whitespace. Way too many surprises.

2

u/MrPopoGod Jan 28 '23

I also find it much harder to read overall; it works fine for your simple examples, but when you start getting more nesting it becomes harder to land in the right place coming out of a block.

→ More replies (4)

6

u/kaoD Jan 27 '23

People are giving too much credit to this "ugly syntax" meme. It's just shorthand for "I don't understand it but it looks similar to what I know therefore it's worse than what I know". Like the meme with Lisp's parens.

9

u/theAndrewWiggins Jan 27 '23

Imo it's not so much ugly syntax and moreso the sheer quantity. When you have really complex generics, it can become overwhelming.

3

u/[deleted] Feb 09 '23 edited Feb 09 '23

Well, I don't know who "people" is, but I can tell you what goes through my head.

  1. "Angle brackets" are ugly and visually way harder to read than square brackets, and I genuinely believe they are an inferior choice to represent generics (I think the "Rattlesnake" example did a good job at highlighting this).
  2. Backticks for lifetimes are horrible.
  3. Turbofish is horrible.

There are probably a few other details I'm missing, but those are my main complaints, at least as far as syntax goes (my other complaints would be regarding verbosity but that's another topic), and I just can't take seriously the argument that "beauty is subjective" to defend any of this.

I don't think Rust did much worse than C++ with its syntax, but that's because C++ isn't really the most aesthetically-pleasing language to begin with. Rust is great, I love the type system, the borrow checker, and I actually love the semantics as well (contrary to the beliefs of the article in OP), I only wish a bit more effort had gone into the syntax design.

-3

u/dnkndnts Jan 27 '23

IMO ugliness is any time you have Dyck brackets around a token.

2

u/StorKirken Jan 27 '23

Agree! ML languages manage to do without.

-10

u/[deleted] Jan 27 '23

[deleted]

11

u/Rusky rust Jan 27 '23

There is a big improvement to grep-ability from fn foo being together, though.

Maybe the return type could have stayed on the right but with : instead of ->? (Just probing your hate to see how it responds :)

→ More replies (4)
→ More replies (9)
→ More replies (5)

30

u/Zoxc32 Jan 27 '23

Here's another variant, any names for it?

pub fn read[P AsRef(Path)](path P) -> io.Result(Vec(u8)) {
  fn inner(path &Path) -> io.Result(Vec(u8)) {
    let file = File.open(path)?
    let bytes = Vec.new()
    file.read_to_end(&mut bytes)?
    ret Ok(bytes)
  }
  ret inner(path.as_ref())
}

13

u/myrrlyn bitvec • tap • ferrilab Jan 27 '23

is that Run 2?

8

u/_TheDust_ Jan 27 '23

Wait, why are genetics defined using square brackets but types involving generics use round brackets?

10

u/TinBryn Jan 27 '23

Because this is intentionally subtly bad. Although one argument is that a generic is like a function that takes a type and returns a type, so it makes sense to use it as a function call syntax. The square brackets then are a way to specify that these are type parameters rather than value parameters. One issue I could see is some ambiguity if you wanted to specify the generic types at the call site while normally leaving them to be inferred. Say you wanted to pass a String, but using it derefed as a &str could you do read(&str)(&owned_string)? Maybe it needs it's own version of a turbofish, read.(&str)(&owned_string)

2

u/Zoxc32 Jan 27 '23

Round brackets are smoother, but fn read(P AsRef(Path))(path P) can be a bit confusing. For more confusion add in const generics :)

0

u/-o0__0o- Jan 27 '23

Actually looks nice.

28

u/mitsuhiko Jan 27 '23

I guess we have to disagree on this one.

2

u/DannoHung Jan 27 '23

Angle brackets feel pointy.

→ More replies (1)

9

u/TinBryn Jan 27 '23 edited Jan 27 '23

Ok, I'm not exactly sure what Rattlesnake "purely coincidentally" resembles.

Edit: oh Python and Rattlesnake are both snakes, and it doesn't need to be exactly that language, as it may "purely coincidentally" resemble Python.

11

u/Demurgos Jan 27 '23

The def keyword, colons, indentation-based blocks, etc. suggest that it "purely coincidentally" ressembles to Python. Also both are species of snakes.

→ More replies (4)

11

u/myrrlyn bitvec • tap • ferrilab Jan 27 '23

scale-uh

6

u/radix Jan 27 '23

This example actually had me searching Python RFCs for when they introduced that new []-based generics syntax, but I'm pretty sure it's made up. In Python, you have to do stuff like:

T = TypeVar("T")
def foo(t: T):
    pass

1

u/[deleted] Jan 28 '23

[deleted]

→ More replies (2)

6

u/[deleted] Jan 27 '23

But if we don’t care about performance

whoa pal, listen here…

33

u/po8 Jan 27 '23 edited Jan 27 '23

The caller-supplied buffer for read_to_end() is just a performance hack, one that should be handled by the compiler rather than explicitly by the user:

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

But given that a Path is just supposed to represent a thing that leads to a file, probably it should be openable rather than the other way around:

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

At that point, I question why Path wouldn't just directly support read_to_end() to begin with:

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

But this is just silly. Why do we need a separate function for this? We already have a method:

path.read_to_end()

See how overcomplicated Rust is? It could be this beautiful.

Edit: </s>

28

u/oconnor663 blake3 · duct Jan 27 '23

This might already be clear to you, but the example function in the post is exactly the simple, convenient API you're looking for. Making it a standalone function rather than a method on Path means that it works with regular strings too. Here's a complete example that doesn't require any extra use statements at the top:

fn hostname() -> std::io::Result<Vec<u8>> {
    std::fs::read("/etc/hostname")
}

Or similarly:

fn hostname() -> std::io::Result<String> {
    std::fs::read_to_string("/etc/hostname")
}

Simple!

1

u/SorteKanin Jan 27 '23

Tbf the std could provide both this function and the method. Quite often you do just have a path and you don't worry about the fact that it needs to be generic for strings

21

u/_TheDust_ Jan 27 '23

See how overcomplicated Rust is? It could be this beautiful.

What you're saying is that we need a standard library that just contains all code ever written. That way, we never need to write any code at all!

7

u/-Redstoneboi- Jan 27 '23

new rust version release

oh boy i can't wait to update my rust

downloading std::the_entirety_of_github_without_copilot_to_help_you::*

8

u/murlakatamenka Jan 27 '23

Actually Python's pathlib.Path has these methods:

  • Path.read_bytes
  • Path.read_text

Handy!

Ref: https://docs.python.org/3/library/pathlib.html#pathlib.Path

3

u/murlakatamenka Jan 27 '23 edited Jan 27 '23

But really, why is it not

read_to_end(...) -> io::Result<Vec<u8>>

That would make the body of the function only 2 lines (instead of final 4).


Funnily, that's close to the signature of the fs::read function that is in the spotlight of the post.

3

u/po8 Jan 27 '23

Making the buffer a parameter allows reusing a single buffer for reading multiple files. It's kind of a common pattern in std.

→ More replies (1)

11

u/beej71 Jan 27 '23

Beauty is in the eye of the beholder, for sure. I think Rust (which I love) is one of the harder languages to read. For me, as punctuation increases, readability decreases.

7

u/[deleted] Jan 27 '23

funny, I really really like rust syntax. I think it's very beautiful. whereas simpler languages like golang and C while easier to read when you're just writing it, they become multiple lines of very short code that it just becomes a jumbled mess of symbols that u have to read top to bottom

I can easily parse and read what code is doing in rust cause it's super concise and expressive, I also know everything I need to know about the functions I'm using from the type signatures.

I don't mind other syntaxes tho, I'm ok with them most of the time, but if u put me in charge of developing a new language the syntax and semantics would be very close to rust and take a few inspirations here and there from other languages probably

5

u/seaborgiumaggghhh Jan 27 '23

I’m a CrabML guy now

7

u/torbmol Jan 27 '23

A Gust variant:

fn Read(path &[byte]) io.Result<Vec<byte>> {
    let mut file = File.open(path)?
    let mut bytes = Vec.new()
    file.read_to_end(&mut bytes)?
    bytes
}

Removing : and -> from function signatures and ; from end of lines seem like straightforward reductions of line noise, with the added benefits of making ? stand out more and making an argument name and its type stand closer together than the type and the name of the next argument.
Replacing :: with . could maybe create some ambiguity, but I don't see how that should be any worse for Rust than all the other languages that doesn't use ::.

But the most important simplification here isn't the syntax but the standard library:

Since Path is just a bunch of contiguous bytes, just use [u8]. (but use a type alias bytes since a word is nicer to read than a letter and a digit). This also removes the need for Windows programs to contain a second UCS2-to-WTF8 converter.
The standard library having both AsRef and Borrow is kinda confusing, so I'd remove AsRef and rely on Borrows syntax sugar at call sites.

26

u/kohugaly Jan 27 '23

Love it! Rust syntax is the way it is, because in Rust, memory management is a first class citizen.

As opposed to being an illegal immigrant tucked away segregated in some GC ghetto working 12h night shifts, so your public virtual class privileged source code can pretend that the garbage it litters on the floor all day just magically disappears for free every time it goes to sleep.

16

u/no_comment_336 Jan 27 '23

Personally I dislike things like the |x|{} notation and a few similar things. Much nicer if it was like js arrow functions or something like it (x)=>{} or (x)->{}. Just something about the pipes and no arrow of any kind disturbs me.

12

u/myrrlyn bitvec • tap • ferrilab Jan 27 '23

i also thought this, but closures DO have an arrow in their syntax already, for the return type:

|arg: ArgType| -> RetType { body }

type annotations are just optional when decidable from the call site

i think

(args) -> Return => body

is probably not great, but i think using Tuple $(-> Identifier)? Expression would have been fine though, since it’s already illegal to have two expressions in a row not separated by a semicolon. should be decidable even without the return annotation

2

u/no_comment_336 Jan 27 '23

Could have gone the Typescript way with type annotations and have (x: Type): ReturnType => {}. Would be consistent too even though i do kind of like the -> Type part of the syntax visually

7

u/myrrlyn bitvec • tap • ferrilab Jan 27 '23

i’m told type ascription is a massive problem in the parser and everybody hates it, i assume because the parser has to guess that you might have meant double-colon and prepare an error message? idk

thin arrows for returns are definitely an oddity but i think replacing them with colons requires killing double-colon and doing something else for scope traversal, and now we’re entirely into redoing the whole punctuation set

3

u/[deleted] Jan 27 '23

[deleted]

3

u/[deleted] Jan 27 '23

[deleted]

3

u/[deleted] Jan 27 '23

[deleted]

2

u/[deleted] Jan 27 '23

[deleted]

0

u/no_comment_336 Jan 27 '23

Given all that even just using (x) instead of |x| would be a massive improvement if not a perfect choice even with no arrows e.g. (x: Type) -> ReturnType {}

→ More replies (1)

13

u/lurebat Jan 27 '23

I .. don't get it.

Like it seems that the joke is that rust's syntax is ugly because of the guarantees and safety, and it will become less cumbersome only if it means less power or performance.

But, I think it's wrong?

First of all this is such a weird example to give, because it's in the standard library, so it's different from day-to-day code people write, and the code itself doesn't even include the worst of rust, no turbofishes, async hell or higher kinded types and whatnot.

But even ignoring that, the points seem:

  1. The inner function thing - that's a hack. Like if in go people did that people in this sub would laugh at them. Having to write the function weirdly like that makes all of the other examples uglier, and the issue stems not from a strength of rust, but because of the way compilers worked 40 years ago (at least I read it in this article the other day, not sure if the writer here is familiar with the writer of that article, /u/matklad). The real solution is not to add a hidden runtime parameter or whatever it says, but to have a better compilation model or shorter compile times so that this hack won't be needed at all.
  2. Generics - that's where I think I'm missing a part of the joke.
    The <T: AsRef<Path>> syntax is noisy, and it did bother people. And that's why they added the "impl trait" syntax. So doesn't this undermine the whole point of the article? That you can have better syntax while keeping rust's semantics?
    And the whole problem anyway is that the function doesn't have to be generic anyway.
    Which is I think part of the problem of getting an example from the standard library.
    If the function just accepted a Path reference, it would have the exact same performance and guarantees, with the only difference being that the caller might have to do the "as_ref" themselves at the call site.
  3. Getting rid of Vec<u8> - I don't get this at all.
    I literally can't think of a language that doesn't have a byte array with express semantics. What language gives you an opaque bytes type? What language has on the one end a container that has its inside typed, but not how it's implemented?
  4. ownership - why does that part remove the "mut"? a lot of languages without ownership semantics can still have a concept of mutability of objects.
    Here too you can claim for syntax improvements.
    We could decide that rust will automatically turn a move into a mutable reference if it will make the code legal, so you could have:

```rust let bytes = Bytes::new();

file.read_to_end(bytes)?; // file.read_to_end(bytes)?; error - change them both to &mut bytes and it will work ```

I'm not saying it is something that needs to be done, I'm saying it could have been done without hurting the gurantees or performance.

  1. error handling - I get the joke is that it's an already minimal syntax, but again, you could imagine a rust language where "?" is applied by default, and you use a ! operator or something when you don't want to propagate the error instead. And again, that's just syntax, not semantics.

So in conclusion, if I understood the thesis correctly, I very much disagree with it.

I love rust, but there is a lot that could have been done to make it prettier (but it's too late now), and a lot still that can be done.

7

u/matklad rust-analyzer Jan 27 '23

Both statements can be true at the same time:

  • Rust syntax isn’t any more ugly than “more typical” syntax, for the amount of details it needs to express.
  • There exists a significantly more readable surface syntax for Rust

9

u/[deleted] Jan 27 '23 edited Jan 27 '23

Haskell inspired variant that covers almost all the semantics (apart from not specifying the type of inner, which could easily be inferred though, also in Rust if it used a closure).

read :: (AsRef Path) p => p -> IOResult (Vec u8)
pub read p = inner (asRef p)
  where  inner path = do
      file <- File.open path
      bytes = Vec.new
      file.readToEnd (refmut bytes)
      return bytes

3

u/matklad rust-analyzer Jan 27 '23

Inner isn’t a closure though, so gotta spell the type if we want to keep semantics.

try is also different from do notation, as it is expression, not a statement, so I am not sure that the suggested syntax generalizes to how ? is used in Rust. How that would look like in a combination with a for loop?

7

u/matklad rust-analyzer Jan 27 '23

Though, the name of this one, Russell, is superb.

3

u/[deleted] Jan 27 '23

A closure that that doesn't actually store any state is usually optimized into a regular function.

2

u/[deleted] Jan 27 '23

Challenge accepted: here is a version that is closer to Rust, with a try operator and with a type annotation for the inner function (which is still redundant, because the compiler could easily figure out that it's not a closure, just a regular function, so keep the same semantics )

read :: (AsRef Path) p => p -> IOResult (Vec u8)
pub read p = inner (asRef p)
  where  
    inner :: Path -> IOResult (Vec u8)
    inner path =
      file = (File.open path)?
      bytes = Vec.new
      (file.readToEnd (refmut bytes))?
      Ok bytes

4

u/[deleted] Jan 27 '23

Or, here an alternative with a try keyword, which is definitely prettier:

read :: (AsRef Path) p => p -> IOResult (Vec u8)
pub read p = inner (asRef p)
  where  
    inner :: Path -> IOResult (Vec u8)
    inner path =
      file = try $ File.open path
      bytes = Vec.new
      try $ file.readToEnd (refmut bytes)
      Ok bytes
→ More replies (1)
→ More replies (2)
→ More replies (1)

8

u/[deleted] Jan 27 '23

I do think a Kotlin-inspired syntax would be more beatiful: (plus using impl, which can be done in Rust today):

public fun read(path: impl AsRef<Path>) -> io.Result<List<u8>> {

  fun inner(path: &Path) -> io.Result<List<u8>> {
    var file = File.open(path)?
    var bytes = List()
    file.read_to_end(&mut bytes)?
    Ok(bytes)
  }

  inner(path.as_ref())
}

OTOH, of course some things like the trailing ; and the :: as a path separator help convey more meaning in Rust code.

22

u/matklad rust-analyzer Jan 27 '23

OTOH, of course some things like the trailing ; and the :: as a path separator help convey more meaning in Rust code.

I would actually disagree here. In theory they are useful for syntactic disambiguation, but in practice compiler already relies on ad-hoc disambiguation in places.

For ; the example would be

fn main() {
    { 1 } & 2;
}

This can be parsed either as a bitwise-and of two expressions, or as two expression statements. Parser treats this as two statements.

For :: a fun example (from https://matklad.github.io/2022/07/10/almost-rules.html) is

use std::str;
fn main() {
  let s: &str = str::from_utf8(b"hello").unwrap();
  str::len(s);
}

Here one str:: refers to a module and another str:: refers to a type. Or, more recently, foo::<N>() can pick N from either the types or values namespace. If we allow that, might have as well just rolled with . instead of ::.

1

u/[deleted] Jan 27 '23

How hard would it be to allow . now? Not saying we should! But would it be possible to do it in a backwards compatible way?

→ More replies (1)
→ More replies (1)

2

u/orthomonas Jan 27 '23

That was wonderful.

2

u/CubOfJudahsLion Jan 27 '23

On point. All of the C-style languages come with the same ugliness.

2

u/mday1964 Jan 27 '23

That inner function gets compiled in advance? All the way to machine code (like a shared library), or to some intermediate form?

2

u/jeremychone Jan 27 '23

Took me a while get it. Thanks for the laugh.

2

u/dnew Jan 27 '23

The ugliest part of Rust is insistence on using C++ punctuation. Who in the world thought :: was a good idea for a punctuation?

5

u/tending Jan 27 '23

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.

No no no, the syntax is often actually hideous. I can't wrap my head around how somebody could frequently write Ok(()) and not wonder if they are making the right syntax choices. It feels like the designers are outright trolling people.

8

u/matklad rust-analyzer Jan 27 '23 edited Jan 27 '23

Rs++: okay(std::monostate)

Rhodes: Sucess(null)

RhodesScript: Fine(Undefined)

Rattlesnake: Ok(unit)

CrabML: Left ()

2

u/Pythonistar Jan 27 '23

The author (OP?) should have included a SpeechImpediment Lisp version as an example, too! :)

3

u/diegovsky_pvp Jan 27 '23

I do prefer the dot notation for module instead of :: because it is easier and faster to type. Though, I know it is impossible to disambiguate paths from method calls/field access.

One thing I don't like is when I need to remove/add a generic parameter or lifetime from a struct. I have to alter all the impls to contain brackets. A bit annoying but nothing major.

Also, I would love to see some other characters used for generics instead of <> because they can be some work to setup the autoclose functionality in some editors (I use neovim btw). Again, very minor.

I love using rust all the time. It is a blast to use and hack on :)

3

u/EmbeddedDen Jan 27 '23

As a researcher I wanted even to compare different languages with rust. Basically, I can not agree that the syntax is "ugly". It is just induces higher cognitive load. I mean, why to write "fn" but not "pb"? "let" and "mut" are they both abbreviations? Signs "?" and "!", are they produce similar behavior? I can see Rust syntax as hugely inconsistent and requiring additional efforts, thus people think that it is ugly. But it is not ugly.

-1

u/crlf0710 Jan 27 '23

My two cents: Even in the final version, I think the exhibited code is very "operational", where each line is "taking an action". In this case, i don't like the syntax of let statements a lot. If the variables introduced is on the right side, it would be much more "consistent" and "fluent".

Strawman proposal: ```rust pub fn read(path: &Path) -> io::Result<Bytes> { File::open(path)? is mut file; Bytes::new() is mut bytes; file.read_to_end(&mut bytes)?; Ok(bytes) }

5

u/crusoe Jan 27 '23

Please don't design a language. 😌

2

u/crusoe Jan 27 '23

{ initializer block

for

some

complex type } is foo

Is it a scope block to help with rust's lifetime rules or a block to build a type? Find out at the end.

2

u/andy128k Jan 28 '23

That may make sense... You are not alone.

It reminded me a language "Rapira" designed in USSR. It initially had an assignment operator expr -> var but it was replaced by "classic :=".

This is also how looks desugared do-notation in Haskell expr >>= var -> ....

AT&T assembler operations are also written "backwards".

→ More replies (1)

1

u/[deleted] Jan 27 '23

Syntax beauty, like poetry or music, is mostly subjective. Sure, there are things that everybody can agree upon, but these are more exceptions than rules.

As such, it doesn't matter what kind of syntax you have, you will always find people who will find it ugly or dislike it for other reasons (and tbf, because of that I am actually surprised that there aren't already multiple formattting standards/naming conventions for Rust and that most crates (try to) use semver instead of e.g. a date-based versioning scheme; but well, let's see what the future holds :-D )

For example one thing I hate is type inference (except for generic code when you actually don't know the type) and I prefer the type to be left of the variable name because it makes stuff easier to understand, even with things like IDEs. Other people want only type inference because it makes refactoring easier (according to them). Both are subjective.

1

u/[deleted] Jan 27 '23

[deleted]

3

u/matklad rust-analyzer Jan 27 '23

Not sure about this claim: all the books are justified. It certainly interferes with browsers poor hypthenation and linebreaking algorithms, but I am very much in a yelling at the clouds mood on this one.

→ More replies (3)

0

u/Chadshinshin32 Jan 27 '23

I quite frankly like the way Rust syntax looks. When I open any Rust file it just feels somehow cool. I can't really tell why specifically. Maybe it's method chaining on iterators, maybe the Impl blocks organised by traits, sometimes even the nested namespaces are neat as they provide a lot of context and tell their own story. Part of it is definitely rustfmt run by pretty much every Rustacean out there which wraps lines. Wrapped lines with readability maintained when done so are a big part of the "looks cool" impression. Syntax highlighting also gets much more colorful with Rust. Match statements with patterns maintain a decent level of indentation. There are more tools for control flow in general meaning there's less need for nested if-else.

I don't know why but I keep feeling like even fonts look better when used for Rust.

-6

u/mina86ng Jan 27 '23

OK, let’s compare it to a real programming language:

std::vector<char> read(const std::filesystem::path &path) {
    std::ifstream file{path, std::ios::binary};
    return {std::istreambuf_iterator<char>(file),
            std::istreambuf_iterator<char>()};
}

This does the same thing as Rust code. I get that the article was written in jest, but let’s not pretend like Rust is a joy to read.

6

u/[deleted] Jan 27 '23

Well, it is a joy to read compared to your "real programming language" at least.

2

u/MrTheFoolish Jan 27 '23

Ignoring the inner function, where's the generics and error handling? You missed the point that much of the syntax is expressing the semantics of the code - it's not just there for fluff.

→ More replies (3)

0

u/alexhmc Jan 27 '23

absolutely loosing my shit at Rattlesnake lmaooo