Hacker News new | past | comments | ask | show | jobs | submit login
An Intern's Experience with Rust (microsoft.com)
125 points by discreditable on Oct 22, 2019 | hide | past | favorite | 126 comments



> First lesson: Rust isn’t as hard as I expected

We're going to see a lot more of this. As the language has matured, the Rust folks have paid great attention to easing the learning curve. Even with the borrow checker, it's not that far from Java or a scripting language these days. Most people won't hit lifetimes early on in their use.


> Most people won't hit lifetimes early on in their use.

Depends on what you're doing, doesn't it? If you're using Rust for its original design-goal purpose—as a systems language—then you're probably writing some kind of daemon that keeps persistent in-memory state between RPC requests from clients; or some kind of UNIX-ish CLI tool that reduces over IO streams. And as soon as a request, or a stream event, wants to persist some state into a longer-lived scope, lifetimes rear their head.


A junior programmer isn't going to encounter this unless they're trying really hard.

Someone with the knowledge of how to write code like this is already familiar with the problem domain and the vocabulary. They'll know enough to be able to find out how to get this performance out of Rust.

New programmers aren't going to look for this or need it right off the bat. They'll still be able to leverage a modern, safe language with lots of features and an extremely compelling package manager.


Or on a more basic level - trying to store a ref in a struct. There's plenty of ways to not do that or think about the problem in other ways, but I think it's fairly easy to accidentally run into that problem early on.


I don't know. I recently took a Rust course and our day three project was to build a simple multi-threaded webserver. The really easy kind in C where you accept a connection and then spawn a thread to handle the request so the main thread can get back to accepting new connections ASAP.

This turned out to be quite challenging in Rust, even when starting from a working single threaded server. Threading the needle on borrows was quite a challenge.


Interestingly, a multi-threaded web server is the final chapter of "The Rust Programming Language" (full code is at the end of https://doc.rust-lang.org/stable/book/ch20-03-graceful-shutd... )

It ends up being pretty short, though we also build a threadpool by hand. If you used a package, it'd be even smaller. (I like scoped_threadpool myself) (And the FnBox stuff will go away once I write an update for it, it's no longer needed.)

It does the same: grab a connection, spawn the thread. There's virtually no borrowing.


Having used Rust for several months, I'd say that multithreading is still one of the biggest pain points (strangely, as it's also one of their headline features). The only thing I've encountered so far that's harder is pointer-juggling data structures, but at least in that case you can mostly just use the (excellent) ones in the standard library.

The "crossbeam" library helps a lot: https://docs.rs/crossbeam/0.7.2/crossbeam/ It's the only way I know of to have thread closures with a less-than-static lifetime, which helps a ton with borrowing/moving and also general encapsulation: https://docs.rs/crossbeam/0.7.2/crossbeam/fn.scope.html.

Once you get the hang of it you can move forward pretty well, but the ergonomics around Rust concurrency, especially in the standard library, really need some work if they're going to continue focusing on it in their marketing materials.


Did you learn C (or any other programming language) by writing a multi-threaded web server in it ?

The Rust book actually ends by showing you how to do that, but... there are many reasons why that's the last chapter in the book. Starting there is definitely not the most efficient way of learning Rust.


I did for golang. Assuming you know how to program in general it's a very basic task.


Ehh... it's a basic task as long as you never touch shared mutable state. As soon as you have to do that, you need to work out what the abstraction for dealing with shared mutable state in your language of choice is.


I mean most languages you don't. You lock do some stuff then unlock. Rust prevents a whole lot of bugs by not making it this simple but that means if you pick anything with threading as a task to jump into the language your going to have a much harder time.


Rust `Mutex`es are simpler to use than C++ ones.. so if you just `Mutex` your shared state away, writing an app on top of `actix-web` or `hyper` or `tokio` is dead easy once you learn how to use the framework, particularly if you have already done that before in Go.


Did you just use Go, or did you write your own Go implementation including its goroutines run-time and then write a web-server on top?

Because if you did just use Go, in Rust you can just `cargo add actix-web` (or `tokio`, `hyper`, `async_std`, or any other goroutine-like run-time) and write a multi-threaded web-server in approx. one line of code.

What the Rust book example shows is how to implement what the Go language uses inside its run-time for running goroutines, which is a much harder thing to do, and very different, even though that's something that every good web-server uses somewhere in its stack.

Learning Rust by using `actix-web` to build a simple web-server is ok, but learning Rust (or any other programming language, really) by implementing `actix-web`, is not a very efficient way of learning the language.


I had the same initial reaction. I wanted a tokio UDP server that delegated to other things. Working w/ tokio streams, futures, async (which I understand are all still in beta) was quite challenging to make sure they could all get back to the read/write of the UDP socket. Basically I end up having to split UDP read/write then concat/join disparate streams or use mcsp channels to fan everything out and get it back to a single stream.


Personally I think languages with tracing GC are much more productive for those kind of scenarios.


It depends on what you're doing. If you're writing sorting algorithms, it's not far from Java (C++, really, but). If you're writing something multi-threaded, or asynchronous, or with polymorphism, it's very different. If you're writing data structures that involve pointer-juggling, it's extremely different unless you bail out and use unsafe!{}.


I checked out rust around 5 or 6 years ago, and it was a mess of sigils for the borrow checker. Things have improved significantly since then. If you haven't checked it out in a while, it may be worth another look.


Anybody else found the last variation annoying from a C perspective? I find that people coming from a web background don't mind writing complex statements in function parameters (seems like due to writing lambdas/closures and such encourages this kind of programming; maybe it makes sense in a lazy evaluation environment), but people with a C background use the last-but-one variation where you use an intermediate variable to capture a prior result to use as a function parameter. I myself prefer the last-but-one variation and it's a pet-peeve of mine that people write large functions/expressions/statements as function parameters when not needed all in the name of conciseness -- I think it affects readability.


In my Rust code I go back and forth. Coming from JavaScript, I started out trying to do everything in functional expressions - which Rust does as good a job of accommodating as it can - but Rust often requires so much more verbosity (.expect().unwrap().clone()...) that I found myself using local variables again.

And what I realized was that, as fashionable as it is to say these days that imperative code is the devil, Rust's (deeply) const-by-default local variables remove one of the main reasons people have for nesting their expressions inline: avoiding the creation of state that has the chance to be mutated. And I also found that giving names to those intermediate steps can really help improve legibility. For me, Rust has made imperative code cool again, while also allowing for maximally expressions-based code (even more than JS in some ways!) where it makes sense.


I probably would not write the code that way either, but it's really hard to say how I would, given that it's not a realistic example.

This is one of the hardest parts about writing examples: you want something that's real, but also not long. Very difficult.


Generally, I like to keep the temporaries. Mostly it is a consequence of getting burned and trained by the Visual Studio debugger; trying to put anything too complicated into the watch window or even evaluating it in the immediate window tends to be an exercise in frustration. It's also easier to step into the sub-expression/function call if need be when I do this, which is not always the case if it is a parameter to another function.


I feel like there is some law in the universe that says that if I don't use an intermediate variable, then I will for certain need that intermediate variable some time later when I'm debugging.


In particular, you can give the intermediate result a useful name (using something more descriptive than 'n' would have helped in the example)...


You mean, you have to give the intermediate results a useful name.

In my experience, this gets ugly rather quick.

But yeah, I hope the pipeline operator comes soon, chaining is nice, but nesting can get quite messy.


>> it's a pet-peeve of mine that people write large functions/expressions/statements as function parameters when not needed all in the name of conciseness -- I think it affects readability.

Worst thing ever. I was trying a gfx-rs example to do some drawing. One smallish function took another function as a parameter (a closure actually) so they just wrote an entire drawing function inline. The pipes helped clue me in that something strange was going on.

My own code pulled that function out into a clean definition eventually. As it should have been in example code.

It's not in the name of conciseness. Not sure what it is, but it's not that.


That looks a bit unusual even from a FP perspective. Functions get passed to functions all the time, just not defined inline very often. Unless it's the only one parameter or a very simple closure, then it could still look clean and readable.


I use almost no lambdas in my functional code. I don't understand why they're thought to be paragons of FP in non-pure language communities. Maybe because there are no operator sections, currying, and the syntax is more verbose so it's not as easy to define helpers.


While i appreciate the type safety that Rust may provide (even if it is overblown and only just a little, it can still be useful - e.g. while i prefer C, i do like C++'s "enum class"), my two main issues are that it is only a single implementation (and that implementation looks is too Unix-y with the Windows versions looking bolted at the side) and -especially- too slow.

I get annoyed when my C builds take more than a few seconds and a major reason i avoid C++ for my own stuff (C++'s complexity is another reason) and i hear that Rust has worse compile times than C++. This is enough for me to stay away from it.

But if Rust manages to get an implementation that feels at home at Windows with a full IDE and debugger and also gets around C-like compilation speeds, then i'd like to check it out.


> and also gets around C-like compilation speeds

Isn't that possibly asking for a bit much? The Rust compiler is doing significantly more than a C compiler, so why would be expect it to have C-like speeds? Should we not also expect C to have compile speed improvements in that period, which might leave them relatively unchanged with respect to each other?

It feels sort of like you're comparing a Mercedes sedan and Land Rover. Both very capably vehicles, but targeted to excel in slightly different circumstances, and when people suggest a Land Rover for rougher terrain, you're noting that when they can provide the same acceleration and gas mileage as the sedan then you'll be interested, which is both unlikely to happen and missing the point, since an off-road vehicle can do things a sedan just can't. Preferring one over the other is fine, expecting one to be superior to the other in every way is a tall order.


That keeps getting brought up as reason for Rust compiler slowness, e.g. complex language requires long compile times.

So then lets pick D, Delphi, .NET Native, Eiffel, Ada as examples of toolchains from complex languages that compile faster than rustc is currently capable of.


> complex language requires long compile times.

My point is not so much that the language is complex, but that the compiler does a lot more stuff along the lines of correctness checking. If you take a C or C++ compiler and add in the equivalent level of checks that Rust is doing, can we expect it to be faster than the Rust compiler?

Are any of the other examples you're putting forth doing things of the same complexity? I know many of them have their own ways of providing assurances, but I know some of them are not compile-time (or at least not more than inserting instructions for run-time checks). You would know better than me, since you've expressed previously that you have experience with many of those, at least at a cursory level. Can you comment on what they're doing at a compile time level that's approaching or surpassing the complexity of the borrow checking in Rust?


Regarding C++ compilation, here is the latest blog entries regarding Visual C++ compilation speedups,

https://devblogs.microsoft.com/cppblog/improved-linker-funda...

https://devblogs.microsoft.com/cppblog/msvc-backend-updates-...

Another ways of improving compilation times:

D has fast compiler as reference implementation, leaving the hard work for gcc and llvm backends.

Eiffel uses a JIT for development, with AOT compilation via the C or C++ system compiler for production releases.

.NET Native, which now also supports F# (so H-N comes into play) takes advantage of MSIL, the development builds are a bit lightweight in optimizations and only the deployment via the store does the heavy crunching of optimizations, based on Visual C++ backend.


None of which do anywhere near the level of optimization that LLVM does. They also don't do H-M type inference or borrow checking.


FWIW i do not care that much about having LLVM's optimizations. As long as there are a few decent optimizations to do most of the stupid stuff i'm fine - i can do the rest myself whenever (and if) needed. It is only a very tiny part of the codebase that would need heavy optimizations anyway, yet having to pay (in terms of compilation time) for that for the entire codebase is an absurdity that we just seem to got used to.


So you have measured ldc, F# .NET Native, Delphi LLVM backend, Eiffel compilation via clang?


I don't understand what your point is. I think you're trying to imply something like "rustc is fundamentally misdesigned and it would be easy to make huge performance improvements", but there really is not much low-hanging fruit left in rustc. The fact is that H-M type inference, borrow checking, and optimization take time. There is no magic bullet that other languages do to compile quickly that Rust does not, other than not having Rust's features or not doing optimization. There's even an entirely separate compiler implementation now, mrustc, which as far as I know is not significantly faster than rustc.

There was a time when rustc could be called a relatively slow compiler for what it does, but not anymore. It's actually a reasonably fast compiler now. I expect small wins to continue in the future, but the only remaining foreseeable large win is Cranelift.


I didn't say it is easy, just that there is still room for improvement.

So here go my couch coach ideas.

You already mentioned one possible improvement, I don't need to compile with all optimizations turned on on debug builds.

Secondly, combine JIT for development with AOT compilation for release deployment, e.g. Eiffel / .NET Native.

Thirdly, make use of a build cache across crates, including third party dependencies instead of building the whole world from scratch.

Combine that with an incremental compiler/linker, like Visual C++ does, where the team use Unreal, CryEngine, Windows, Office and other similar sized projects to measure build improvements.


Optimizations are not turned on for debug builds and never have been. The issue is that LLVM is still pretty heavyweight even when not optimizing. An entirely separate backend is needed.

As Steve mentioned, there is a global build cache already.

We have an incremental compiler. It's not as incremental as it could be, however.

We already have an extensive system, crater, that can measure build time improvements.


> Thirdly, make use of a build cache across crates, including third party dependencies instead of building the whole world from scratch.

This already exists and is easy to use: https://github.com/mozilla/sccache


Thanks for the heads up.


I agree with your overall point, but

> but the only remaining foreseeable large win is Cranelift.

I'm not so sure about that, given the huge work going on to make rustc truly incremental.


And incremental compilation is more than virtually all the listed compilers can do.

I think the issue is more that not many people are using those languages in anger (except for Delphi I suppose, but Object Pascal is a comparatively simple language with no H-M inference that also does very little optimization). People don't hit the limitations.


> Isn't that possibly asking for a bit much? The Rust compiler is doing significantly more than a C compiler, so why would be expect it to have C-like speeds?

I do not expect it, i'd just want it to be fast for me to want to use it. I mean, i do not really care why it is slow (outside of curiosity, but not as an for accepting it), i care that it is slow.


> I get annoyed when my C builds take more than a few seconds and a major reason i avoid C++ for my own stuff (C++'s complexity is another reason) and i hear that Rust has worse compile times than C++. This is enough for me to stay away from it.

One of the main reason we stopped using C++ is because Rust compiles much faster than clang. That was 3 years ago, and in this time frame Rust has improved compile-times significantly (e.g. reductions of ~30% per year).


If you use similar feature sets rust compiles significantly slower.


With C++ I don't need to compile 3rd party dependencies from scratch.


For what it's worth, Rust will soon improve its linking stage by switching to ldd, as part of upgrading to LLVM 4.0. From what I've read ldd can dramatically reduce the time needed.


You probably mean 9.0.0, 4 is several years old. Or is Rust really still using something pre-4?


No, we run very close to the latest release; we also have our own patches that we try to upstream.

We do support back a little bit, to help distros.


My bad, I guess I was confused earlier about something else.


Rust has some of the best Windows support, in my experience. It uses native ABIs and object/debuginfo formats (not MinGW), and I debug it from Visual Studio.


It does well on Windows, but I still look forward to VS mixed mode debugging like .NET/Java and C++ IDEs are capable of, and being as productive as C++/WinRT to write COM/UWP components.


VS mixed mode debugging doesn't need language support. You can already call back and forth between Rust and C# and the debugger will handle it fine.


On the other hand, I think user time is more valuable than developer time and when building complex/efficient software expecting lightning builds isn't just unwarranted, it's selfish.


> I think user time is more valuable than developer time

Developers are users of their development tools.

> when building complex/efficient software expecting lightning builds isn't just unwarranted, it's selfish.

This is wrong. There is no rule that says that you cannot write fast applications with fast builds. It is just that we got used to C++'s slow build times.

This sort of thinking even falls flat on its face when you consider that C provides similar optimization opportunities as C++ (and in pretty much every modern compiler it uses the same backend and most - if not all - optimizations) while compiles much faster.

But beyond that most of the codebase of a decently sized application does not need more than the most basic of optimizations - it is only a tiny tiny fraction that needs that, yet with modern C/C++ (and most other) compilers, the entire codebase suffers for it (i think only Visual C++ allows you to control optimization settings on a per-function basis, but i haven't encountered any codebase that actually uses this outside of temporarily disabling optimizations for debugging and/or working around bugs).


Users typically want more features and lower cost a lot more than they want maximum performance. Faster builds help developers deliver more features at lower cost, so faster builds potentially have a bigger impact for users than faster software getting produced at the end of the build.


Yeah, but when there are other languages that also AOT compile to native code, with faster compiler toolchains, it is already a deciding factor when choosing languages.


People promoting Rust tend to assume that it's the concepts in Rust that don't exist or aren't explicit in other programming languages that are the hard part. In my opinion, that's not true. It's the syntax used to make those concepts explicit. To use the example from the article: lifetimes are not a hard concept to grasp. Knowing when the Rust compiler will require you to make a lifetime explicitly part of your type, vs. when you can elide it? Hard.


> To use the example from the article: lifetimes are not a hard concept to grasp. Knowing when the Rust compiler will require you to make a lifetime explicitly part of your type, vs. when you can elide it? Hard.

There are only three (or is it four?) rules about when you can elide. Personally? I never write lifetimes, and then the compiler yells at me, and then I fix it by adding them. It's not something that you need to actively think about. You see

    error[E0106]: missing lifetime specifier
     --> src/lib.rs:2:8
      |
    2 |     x: &i32,
      |        ^ expected lifetime parameter
and you add it in.


Lifetimes trip me up in Javascript all the time. I think that I have all my callbacks and/or promises lined up and dealt and my app runs fine. Until it doesn't and I discover that there is some corner case in my 5 nested deep callback hell where I didn't catch some DB timeout. I'm looking forward to that never happening again in Rust.


Using types as an analogy: Python and JavaScript programmers know the concept of types, but don't have to be rigorous about it. They can even make a mess of it, and use runtime reflection as a crutch. OTOH with a statically typed language you have no choice but be very precise and explicitly think about types.

It's the same with Rust ownership and lifetimes. C programmers know the concept of ownership, but don't have to be rigorous about it. They can use `char *` for both owned and borrowed stings, and make a mess of them (using `need_to_free_that_pointer: bool` as a crutch). When Rust has owned String and borrowed str, it's a surprise.

Sure, there's syntax to learn, and it has a few obscure bits if you're into generic programming. But the fighting with the borrow checker Rust is infamous for comes from using a C mindset of "will it crash or leak?" rather than "is this owning or borrowing?".


"there's syntax to learn" is an understatement. There's a lot of syntax to learn, and you can't get away with using a subset of it.


There are 4 places where you use lifetimes. It's really not hard syntax-wise.

I teach Rust workshops and I find that novice users struggle with this, because they think of references as pointers, so they use them in places where they don't make sense (e.g. in owned structs or returned from constructors), and then desperately try every possible lifetime syntax to force through an impossible borrow.

In practice, once you "get" ownership, it's rare to use any lifetime syntax at all! In a codebase I have open right now (which is in production in 194 datecenters) only 3% of uses of references have a lifetime annotation, and it's just `<'a>` and `&'a`, nothing more complex.


"Rust code is wonderful to write and read"

Writing I'm not sure, reading it's just awful, everytime someone show his project on HN I check github and I'm baffled how the code looks bad as a human reader. (){}<>''-_


I feel like, whether with human or computer languages, you can only really say what the experience of reading a language is like once you're fluent in it; and you can only compare the experience of reading a language to another if you're fluent in both of them.

Like, to me, the human language of Vietnamese looks like a mess of accents, and I'm baffled by why they're necessary. But I'm not a fluent speaker of Vietnamese; and I can imagine that, for someone who is fluent in Vietnamese, and another sibling language like Chinese, maybe the extra accents actually compare very well and help readability compared to e.g. the reduced set of accents in Chinese pinyin.

Likewise, I would suspect that for someone fluent in both C++ and Rust (i.e. someone who knows how to write code that takes advantage of arcane language features in both), Rust might be more readable than C++ because of some of the initially-offputting syntax, whereas C++ might seem less readable because it eschews extra syntax in favor of using template constructors for everything.


This reminds me of my experience with TypeScript. I kept noticing it and feeling a little irritated by its popularity. God it added so much noise to the code. JavaScript was fine for years. I got stuff done. I caught the bugs and debugged them.

Once I finally took the plunge and committed a full project to learning TypeScript, I grokked it. I no-longer saw noise, I saw signal. I saw details about interfaces that I used to have to infer, document, or reverse engineer if I forgot.


Incidentally - I have the reverse issue.

I had (still have!) trouble reading JS, because the vast majority of things I've worked on have been in statically typed languages. JS was baffling because I always have to try to infer/reverse engineer things that I can just read off in the languages I'm most familiar with.

The only way I really could handle dynamic stuff was for things like scripts, where I could mostly remember the entire set of types.

I've gotten better with JS after using some TS, but that difficulty still exists. Correspondingly, I'm delighted by Rust - sure, it's a verbose language, but that puts everything I want to know right there for me to read.


This reminds me of a quip relating to evaluating the morality of historical figures (e.g. whether they were basically good or basically bad) - you're a tourist. You're evaluating someone in an entirely different culture.

So like you said - same thing with languages. You could probably substitute "Japanese text" or "Arabic text" into the original comment and see a little more clearly that fluency does matter.


Those that are fluent in a programming language have merely become accustomed to the unpleasantness and it makes sense for them. There are people which genuinely think that complex mathematical notation can be beautiful, but that opinion's not shared by most humans.

In other words, there's beauty, and there's acquired taste and it's legitimate to point out this difference.


> There are people which genuinely think that complex mathematical notation can be beautiful, but that opinion's not shared by most humans.

Most humans frankly don't understand what's needed to capture complex mathematical concepts, so that's not at all surprising. The same argument can be applied to your objection to syntax, and this merely reiterates the OP's point: until you're fluent in a particular language, you have no idea whether the syntax is necessitated by some complex but necessary semantics that you simply don't know about.


I think there are other factors. I liked lisp without even knowing it. Because my brain wanted the limited amount of syntax possible.

Your mental model will affect how you feel about a language.


Rust is very middle of the road when it comes to using weird symbols all over the place. Just take a look at some C++ or Objective C code...

I think this is more a problem with what you're used to. You see the same criticisms of every language: Basic dialects are too wordy, Lisp dialects too bracketty, C dialects too symbolly, etc. etc. It's hardly the measure of a language.


Learning Python on the side right now and I can’t help but add {} or ;’s all over the place. I have to stop my self a lot. Coming from JavaScript (and a little java) myself it’s much more pleasant to use Python. You mean I can just indent a function?

Java was the language I learned during CS101 in school and I always hated the “don’t worry about this part right now just write static main void... or the equivalent”. Python code is cleaner too look at but Java has it’s reasons for the verbosity.


until you realize that you can't do multi-line lambdas because whitespace sensitive languages are awful. Note how no new language chooses to follow this nonsense.

The braces allow you to be explicit with what you want. Rust's {} are a feature. They let you see exactly which scope you're in. With Rust's memory management, you then know exactly when a variable will go out of scope and be cleaned up.


I’ll have to remember that for later use. The reason I am learning Python is to setup some home automation using Raspberry Pis and a flask based api/app to complement the data they collect.

You peaked my interest. Guido van Rossum on the subject:

> But the complexity of any proposed solution for this puzzle is immense, to me: it requires the parser (or more precisely, the lexer) to be able to switch back and forth between indent-sensitive and indent-insensitive modes, keeping a stack of previous modes and indentation level. Technically that can all be solved (there's already a stack of indentation levels that could be generalized). But none of that takes away my gut feeling that it is all an elaborate Rube Goldberg contraption.

https://www.artima.com/weblogs/viewpost.jsp?thread=147358

As a side note, there are more than 100x the number of jobs with Python in the description vs rust. Out of the 30 or so jobs with Rust in the description most are for automotive techs.


> As a side note, there are more than 100x the number of jobs with Python in the description vs rust. Out of the 30 or so jobs with Rust in the description most are for automotive techs.

Rust 1.0 was released May 2015 [1]

Python 1.0 was released January 1994 [2]

That's over 21 years that Python has had a chance to work its way into the industry and into almost every field imaginable. I'd wager that many careers have revolved around the Python language at this point.

Rust is just getting started. The volume of press is the first sign that it's about to take off. It can take years for existing enterprises to approve a new language for use. Give it time.

[1] https://blog.rust-lang.org/2015/05/15/Rust-1.0.html

[2] https://en.wikipedia.org/wiki/History_of_Python#Version_1


> you can't do multi-line lambdas because whitespace sensitive languages are awful.

like Haskell ?


Unrelated to whitespace sensitivity but modern languages choose to make semicolons optionals. E.g: Kotlin, swift, go that adds to the list (js,python, ruby, etc)


Other than block syntax... how's ObjC bad? It's verbose, I'll grant you that, but I wouldn't lump it in with a language that's enforcing lifetimes and what not.


It's really not middle of the road. I wrote both Objective-C and C++, and both (C++ templated code aside) are more readable than Rust.

Objective-C in particular was verbose, but the symbols made sense and their meaning could be understood from the context. In Rust you either know what the symbols mean, or you do not understand the code.


Most of the symbols used in Rust are used for the same thing as they are in the C family of languages. And if you don't recognize them, https://doc.rust-lang.org/stable/book/appendix-02-operators.... is there to help.


> In Rust you either know what the symbols mean, or you do not understand the code.

This is just part of knowing a programming language. Readability to people who don't know a programming language might be considered an interesting property in itself, perhaps especially for cognitive psychology or education, but it has little to do with programming. It's only accidentally related to a concept of 'readability' relevant to software development.



The blocks extension adds one extra symbol. Same deal with syntax applies as with function pointers in plain C.


Do you know Rust though? "wonderful to .. read" doesn't connote being readable to people who don't program in Rust. Some famously 'readable' languages (Go, Python) can be scanned by the non-fluent, but this is an additional quality which becomes less relevant as you learn the language.

I find Rust readability a mixed picture. Being a Rust novice I sometimes find the details hard to follow precisely (as in: I often wouldn't be able to reproduce the code without error myself). But the type system, stdlib & generics make it sufficiently expressive that it's usually pretty easy to understand broadly how things work. In my journey so far I've been able to follow large library codebases more easily than in other languages I've used.


Is it really that much worse than C or JS? Most popular languages other than Python use () and {}, <> are just for generics like many other languages, " is used for strings, - is just a minus sign, and _ is just used when you need to ignore something when matching just like it is used in Python.

Yes, Rust is a very symbol rich language, but it also is very information dense which makes it easier to read than many other languages once you learn it.


Information density isn't the end goal though, otherwise we'd all be writing into something like APL or its derivatives (K/Q/J/etc). And yeah, if you have ever missed a & in C++ you can easily see why a single character symbol can be annoying (i really prefer C#'s approach where you not only use ref and out in function declarations, but also during function calls - though overall i prefer Pascal's more explicit and verbose syntax).


I think the goal programming languages optimize for is somewhere between, on the one hand, "learnability" / "time to fluency"; and, on the other, "power/expressivity when fluent." When you multiply these measures together, gives you an area measured in "productivity over time of each man-hour invested into the project by population of developers of various stages of fluency with the language" (where for an average developer on the project, any given man-hour is partially spent writing code, and partially spent becoming more fluent in the language.)

APL-likes (and Forth-likes, and Lisps when you use the macro features) are a bit too far on the "power when fluent" axis, at the expense of learnability, and so the total area of the product is small. Might be good for one-man projects, but not for large ones.

Minimal languages like ASM or Java, where there are just a few primitives and everything else is design patterns, trade high "learnability" for low "power when fluent", and so also have a reduced optimization product.

Most languages aim somewhere in the middle, though often with a bias toward the side where they think the optimum might truly lie; for example, Go is slightly on the "learnable" side, and Rust is slightly on the "power when fluent" side, but both seem to be generally more productive per average-programmer-man-hour invested into FOSS projects than languages that more heavily favour just one axis.

(Side-note: I'd love to see some real numbers crunched on this. A comparison of rival FOSS projects to implement some shared standard in different languages would make for a pretty good "natural experiment" to look at productivity over. First thing that springs to mind for me personally are the rival Ethereum clients of Geth (= Go) and Parity (= Rust), but I'm sure you can think of your own examples.)


> where there are just a few primitives and everything else is design patterns

C++ is design-patterns heavy and symbol heavy at the same time.

The heaviest design-pattern language champ would be JavaScript 1.5 with the lack of construct (assuming people work in super-huge JS project with hundreds of developers, not a simple web-app).


This isn't about learnability or time to fluency, it is about readability. The & issue i mention for example isn't about how easy it is to learn about references, but about how readable is when, e.g, you are reading some patch in a code review and you miss someone forgetting it when returning a huge collection which can causes severe performance issues.

These are mostly orthogonal issues to how easy and/or powerful a language is.


My definition of "fluency" is that it's the point where every well-written piece of code in the language is easily readable to you. ("Fluency" of a spoken language is the point where you can speak it and parse-while-hearing it without thinking, so I think this is a sensible definition.)

By this standard, most programmers never achieve 100% fluency in even their favorite programming language, unless their favorite language is one of the dead-simple ones; and there are some programming languages (esolangs, certainly) that nobody is 100% fluent in; and many (the APLs, Forths, etc.) that only a few human beings on earth are 100% fluent in. (That's not to say that there aren't many people who can read and write them—just, not fluently. If you need to look at a programming-language reference more often than you need a dictionary to parse out the meaning of a sentence in your native spoken language, you're not fluent.)

A language that's less "readable", by your terming, is just one that takes longer to become fluent in. Given that the programmers working on any given project are going to be in a mixture of phases in their careers, and thus have invested different numbers of man-hours into language fluency, there's going to be a mixture of fluency levels on any project (where less-fluent readers experience the language as "less readable.") Longer time-to-fluency means that the same number of invested man-hours get you less fluency, and so on projects in languages with longer time-to-fluency, but the same distribution of programmer career-levels, you'll see lower average fluency (or a skew toward the bottom of the distribution, really) and thus more complaints of low readability. Mind you, this isn't really a fact about the language itself, but a fact about the project—if everyone on a project are 20-year veterans of the language, any language can be "readable."

I would note that we can measure time-to-fluency; it's objective. We can teach people a programming language, test them along the way, and see how many man-hours of study it takes to get 100% on the tests. We can't measure a language's "readability"; it's subjective. But it likely has a heavy correlation to time-to-fluency, so time-to-fluency is the measure you should care about if you care about A-B testing programming language syntax features for their effect on "readability."


That's your opinion.

I think it looks awesome. Pattern matching, short keywords, types on the right... It accomplishes this beauty while also carrying lifetimes, pointer types, and traits. I personally like it much better then Java or similar static typed languages.

You can't compare it to a scripting language because it has type information.


> You can't compare it to a scripting language because it has type information.

What do you mean by this?


Rust requires types, pointer types, lifetime bounds, trait classes, etc. for struct and function declarations. They're also used to disambiguate in other places when type inference isn't possible, such as during method chaining.


You could have that in a scripting language, and you could not have that in a compiled language. The difference between the two is the back-end not the front-end.


What are you talking about?

A statically typed language would be hard to reason about without types and it would dramatically impact compilation times.

Why would you include lifetime annotations in a GC'd scripting language? These are features for the compiler and the human to reason about. The language and compiler are intertwined.

While you could divorce the language from the compiler, it would be misguided to do so for many languages and problem domains.

You can hold up TypeScript as an example of a scripting language with types that upholds your philosophy, but its advantage is primarily for humans. I'm unaware of any performance benefits or optimizations it provides. It wasn't carefully spec'd out with a compiler in mind. It's a well thought out cross-compiled superset language. This isn't the case with languages designed for bare metal.

edit: I can't reply with a comment thread this nested. Compilers absolutely inform language design and vice-versa. Bad language design leads to horrible compilers (C++). A good language design process works in lockstep with its compiler design, with careful attention to the AST, the number of passes, analysis, etc. If we didn't think about compilers, we couldn't design languages that could be compiled.


Whether a language is compiled or interpreted has nothing to do with its design. For instance, you can interpret C++ with Cling or compile it with Clang. You can compile Python or interpret it. Same with Rust.

Otherwise, you've conflated a bunch of things. Interepreted languages don't need to have a GC, or lifetime annotations, or anything else, really. Static typing doesn't require explicit type annotations, although it tends to have them, and neither is the domain of exclusively interpreted or compiled languages. And so on.

I also argue there's no meaningful distinction between "designed for bare metal" and "designed for application programming" other than a language that is designed to allow deployment on bare metal has a way for you to poke memory locations and at minimum turn off a GC. Everything above and beyond that is language features or libraries and by no means limits your ability to operate quickly and efficiently in either domain.


Then we are two, I'm afraid. I really would like to learn it and have tried multiple times but it just isn't aesthetically pleasing to read.

Python on the other hand is awesome, and I also begun looking at Go which seem to be quite okay.


I must be in the minority then, because rust's syntax doesn't bother me in the least.

The only exception is the method::<Type>() that's sometimes necessary.

When I write or read rust code the symbols just sort of disappear.


Reading countless rust threads has left me with the opinion that some people are just unadaptable. They cling to what they consider right way too hard. It is not that they are wrong, their knowledge is right for the sphere they work in, but they extrapolate it too far and refuse to get out of their comfort zone.


I actually like `method::<Type>()`, but I did not used to like it when I started learning Rust.

Nowadays every time I have to go back to C++ and read `method<Type>()` I need to parse whether this is `(method < Type) > ())` or something else. Luckily for me, C++ introduced the `method.template <Type>()` syntax... but over that I prefer `::<Type>`.

I also prefer ::<Type> over Scala and D's syntaxes for some reason. I find Scala syntax for this more complex, and I find D syntax too simple.


Do you know Rust at all? Or is that your take knowing how to program in general & not being able to jump in and read at a glance?


That's the punctuation that every C-like language uses.


I'm not really a rust programmer, but I find it quite readable for the most part, one you get used to the unusual data structures.


That is also not how you would write it in idiomatic C++.

The "before" example is C, or bad C++. You are allowed to write bad C++, as you are allowed to write bad Rust. But if you are to compare langues honestly, you should be comparing good code in one language to good code in the other.

The alternative is tendentious but typical, and nothing to be proud of.


I found the article quite reasonable. The author explained that Rust tought them RAII and how they should have been writing C/++.

Rust seems anecdotally better at onboarding new engineers than C/++. We're going to start seeing more articles like this.


I wouldn't expect an intern to write idiomatic modern C++. That looked to me to about what I would expect from a university student that had passing familiarity with C++ from their coursework; there is still an unbelievable amount of "C-with-classes" in active use and pedogogy kicking around.


Given how we used to teach C++ to university first year students back in 1994 - 2000, it still confuses me that there are so bad teachers keeping "C-with-classes" in active use.

Basically, even without the STL, the students would get our department C++ library with the usual stuff (strings, arrays, vectors, bounds checking enabled), and the teaching would be similar to the way that Kate Gregory preaches how to teach modern C++.


Although object lifetime and borrowing is mentioned, he focuses on the pattern expression which he's likely to get in any modern language.

Rust has many great qualities and I've been looking to find a project for it, but it just doesn't seem like the model for long-running processes with immutable data-structures and complex/dynamic/interlinked lifetimes. Seems like dev. effort is better spent on functionality than managing lifetimes, especially given that modern GC is so fast. (And I've seen C++ ports to C# become faster, due to unneeded calling of copy c'tors, and other goodness.)

I can totally see the case for Rust in systems code where lifetime is gated by a system/API call and you want to safely pass buffers around during the duration of the call, for inner-loop gaming, for embedded, etc. But seeing it's en vogue, there are many examples of Rust code where it doesn't seem like the right tool -- but then again, I feel that way about much code written in C++ as well.


Lifetimes aren't a worse version of GC, just like static typing isn't a worse version of dynamic typing. They're different trade-offs.

Rust can express ownership and lifetime of objects statically at compile time, as opposed to dynamically at run time. For beginners this is a new aspect of programming they need to learn, and it adds to the learning curve (same as you need to think about types in C++, instead of winging it in JavaScript), but it has very nice side benefits:

• Deterministic destruction. You don't need to remember about closing handles/cleanups, and all things are freed immediately when their scope ends (rather than "finalized" sometime later). Some languages have `defer` or `withFoo(callback)` to handle these. In Rust it's automatic and more flexible.

• Ownership adds useful information to the program. You know which args will be kept or shared, so you don't get shared mutable state by accident. Libraries don't have to copy their args just in case the caller changes them unexpectedly later. And as a caller you know what each function does to the data you pass in.

The last point is especially useful for passing buffers around, because you control at all times when you give exclusive or shared access, and how long each part of the program can keep them.


> Lifetimes aren't a worse version of GC, just like static typing isn't a worse version of dynamic typing. They're different trade-offs.

This is a completely bogus comparison.

First, static typing is mostly about finding bugs. GC-induced overhead is not a _bug_.

Second, it is theoretically impossible to create an algorithm that checks almost any interesting property of a program in a Turing-complete language in finite time, that doesn't reject valid programs. In most (non-academic) programming languages with static typing compiler checks assumptions about the _data_, and doesn't wade in control flow at all. Data is more or less "static" by itself (types of variables remain the same in most algorithms, with exceptions covered by ADTs), that's why static typing is practically useful.

Lifetime control is a problem on a whole different level. Roughly speaking, lifetime of an object _easily_ depends on whether or not some part of the program terminates, so, from the get-go, your static checker has to solve halting problem, which it can't.

I went through several Rust projects to see how they deal with this. And there are two solutions: 1) sprinkling code with unsafe's 2) resorting to reference counting. GC is safer than both alternatives, and its amortized cost may be less than same for ARC.

Absolute majority of the code written doesn't have hard real-time requirements, and should just rely GC as a safer and more convenient option.


Lifetimes and ownership are about finding use-after-free and double-free bugs. The fact that they enable automatic memory management without a tracing GC overhead is just a cherry on top.

For a long time languages were stuck with the false dichotomy of either trying to be 100% safe (and pay the cost of a VM/sandbox/GC) or said "halting problem" and gave up. Rust solved this dilemma by allowing safe abstractions around unsafe blocks. It works like a charm — I get speed I got from C, but don't have to use gdb.

There's option 3) use program architecture that has clearer ownership and less shared mutable state. Borrow checker doesn't like doubly linked lists or mutual parent-child relationships. Instead you use different containers and DAG structures.

And when you still have to have shared ownership without a clear scope, then you use refcounting. The nice thing in Rust is that (unlike C++/ObjC/Swift) it doesn't always have to be atomic in multi-threaded programs, and objects can be borrowed and used without touching the refcount.


What false dichotomy?

Modula-3, Mesa/Cedar, Component Pascal, System C#, ...

You get the productivity of automatic memory management, RAII, stack and global memory allocation and unsafe blocks just like Rust, with C's speed as well.


> Absolute majority of the code written doesn't have hard real-time requirements, and should just rely GC as a safer and more convenient option

Yes.

Most Rust code deals with "trivial" lifetime issues -- not trivial in the the sense of how it gets used (usage can still be in a "serious" context, such as systems programming) but how the lifetime is controlled. Only certain models lend themselves to be expressed as "borrowing from an owner", and ref-counting will break with general object graphs.

Most garbage is gen-0, which gets recycled with little overhead, and when scavenging is needed its fairly quick and tunable these days and can optionally take advantage of multiple cores.

Another "trick" I see in Rust code is to use a symbol-table of sorts to represent object graphs using names and lookups, but you're just inventing your own, slower version of object references, and then there's the issue of who owns the graph symbol table, so you're only passing the buck in the general case.


You don't spend developer time "thinking" about lifetimes as you write Rust, it becomes totally automatic, no more than you spend time "thinking" about functions in C#. The benefits you get far outweigh the negligible overhead, providing things like data race prevention across threads in addition to memory safety with incredibly low overhead. Modern GCs are faster but they're by no means free, and totally unnecessary if you just give the compiler the information it needs to do its job statically instead of continuing to strap more rockets to the GC rocket-powered horse. It's the fastest horse but it's still a horse, not a car.


> Modern GCs are faster but they're by no means free, and totally unnecessary if you just give the compiler the information it needs to do its job statically

These are not mutually exclusive.

(1) A GC'd language can still do static analysis at compile-time or JIT-time to avoid GC all-together when possible (2) GC'd languages can have RAII/static-scoping capabilities such as immutable value-typed (non-ref) structs which require initialization and using{}/after blocks for scoped lifetime (3) GC'd languages can have owned-object semantics like C#'s Span<> to avoid GC (4) You can use unsafe and pointers and GC'd languages a well

So you can still program with an eye toward efficiency for any code that shows perf issues using many of the same constructs you do in Rust, in a GC'd language -- but be able to fall back on GC when lifetime issues are complex or hard real-time perf is not an issue.

Again, I'm not bad-mouthing Rust at all -- I'd far prefer it to C++ in the context where the latter is appropriate.


Try to write a sizeable Gtk-rs application to see how automatically it gets if you don't create your own clone macro to deal with all the Rc<RefCell<>> instances, like the Gtk-rs samples show.


I’m not sure a single example of a C wrapper not idiomatically designed is a good example of the potential of a language. It’s like pointing to the source for any given STL implementation as quintessentially C++ coding style. RefCells tend to be considered an anti-pattern AFAIK since they leave invariant checking to run-time. It's meant as a hack/patch if you can't actually design it idiomatically for some reason, such as in this case, FFI to a C programming model.


I can point to other UI examples, like game engines written in pure Rust, using array indexes with clocks on access to invalidate possible use-after-free accesses with outdated indexes.


Rust is pretty neat and I would like to use it more. It seems like it could be the ideal tool for performance-optimized software, like C++ was 15 years ago. Unfortunately, there isn't all that much performance-optimized software that I am writing nowadays.


The person writing this seems to be a very clever and capable individual. That being said, I would imagine a company the size of MSFT would have a senior engineer coding "a security critical network processing agent". That sounds like a lot of responsibility for someone inexperienced.


the `{y:2, x:1..=4}` syntax is confusing, is it really range checking ?


Yes, it says "match if y is two, and x if it's between one and four, inclusive".


Weird couldn't find it in the Book.


We don't cover this exact combination of features directly in the book; this falls out of composing the "pattern match" feature with the "range" feature.

It's also possible that you're reading a version of the book before the ..= syntax was introduced; it hasn't been there since 1.0.


Interesting.

Now if only the images with code actually had the snippets in code form I would be able to share the blog post to other people.




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

Search: