Hacker News new | ask | show | jobs
Rust for Professionals (overexact.com)
126 points by keewee7 1262 days ago
12 comments

An important “unblocker” for me when learning Rust after decades of other languages was internalizing that assignment is destructive move by default. Like many Rust intros, this sort of glides past that in the “ownership” section, but I felt like it should be a big red headline.

If you’re coming from C++ especially, where the move/copy situation is ridiculously confusing (IMO), but also from a simpler “reference by default” language like Java, this has profound impact on what’s intuitive in the language.

For the C++ comparison, this is a pretty good article: https://radekvit.medium.com/move-semantics-in-c-and-rust-the...

Also very important realization is that things that are moved around (assigned to variable, moved into or returned from a function, kept as a part of the tuple or a field of a struct) must have fixed and known size. And the variable itself is not a handle. It's a fixed sized area in the memory that you named and moved something into it.

This makes completely logical why some things must be Box<>'ed and borrowed. Why you cannot treat Trait or even impl Trait like any other type. Why sometimes it's ok to have impl Trait as your return type while in other cases it's impossible and you must Box<> it.

Third important realization is that things borrowed out of containers might be moved 'behind the scenes' by the container while you hold the borrow, so you are not allowed to mutate container while you are holding borrows to any of its contents. So it's ok, to instead hold indexes or copies or clones of keys if you need.

Another observation is that any struct that contains a borrow is a borrow itself. Despite using completely different syntax for how it's declared, created, it behaves exactly like a borrow and is just as restrictive.

Last thing are lifetimes, which don't have consistent (let alone intuitive) syntax of what should outlive what so are kinda hard to wrap your head around so you should probably start with deep understanding what should outlive what in your code and then look up how to express it in Rust syntax.

Rust is syntactically very similar to other languages, but semantically is fundamentally different beast. And while familar syntax is very good for adoption I'd also prefer tutorials that while showing the syntax explain why it means something completely different than what you think.

As someone who hasn't looked at Rust, reading all this makes me never want to. Am I missing something? Is it actually easier to reason about the language than it feels like from reading this text?
This is just stuff that's flagged automatically by the compiler. You don't need to "reason" about it, the compiler will tell you how to fix it if it does come up.
I found the copy/move situation in Rust to be far less intuitive than in C++. In C++, move semantics are obvious because they rely on std::move and the && operator, whereas in Rust, similar behavior seemed to depend on the object type. Even more confusingly, Rust has its own move operator as well, despite destructive move being the default behavior for assignment.

I found it frustrating enough that I put the language down and just went back to using C++.

> In C++, move semantics are obvious because ...

In Rust it's also obvious because every = is a move. Confusion comes from tutorials pretending for too long that it's not.

> whereas in Rust, similar behavior seemed to depend on the object type.

It's best to think of that as an exception to the rule created specifically for numbers and other similar small, cheaply copied things.

If you need to move `a` but `a` has a trait Copy then you copy instead.

> Rust has its own move operator as well, despite destructive move being the default behavior for assignment.

I don't think that's true? Rust has a `move` keyword but it's a part of closure definition that makes it take all the variables from its environment by move, even if it doesn't need it. Unless you are talking about something else...

  > In Rust it's also obvious because every = is a move.
False, depending on the presence of an explicit type annotation on the left-hand side, an = triggers either a move or a reborrow.
How does it look like? Do you mean this? https://haibane-tenshi.github.io/rust-reborrowing/

If you assume = always moves you can just never use automatic reborrowing (do &mut * instead) and you'll loose nothing.

I don't think that's a very common pattern, fully optional and it can be just treated as another exception to the rule, just like Copy types. You don't have to manually write .copy() in some cases, and you don't have to manually write &mut* in some others. But that's what happens. Move is still done.

So that's total of two exceptions. Way easier to be tackled individually when the time comes than assuming = means something else, or something complex from the start.

There's a way of thinking of `Copy` which makes it not an exception: `Copy` variables/places are simply not rendered invalid/uninitialized when they are the source of a move operation, unlike non-`Copy` sources. They're still moved from bitwise like all other rust values!
I've posted similar minimal examples before. The first one triggers a move (and then fails to compile), while the second one triggers a reborrow. Sure, you can explicitly write out every instance of reborrowing with `&mut *`. That would require you to understand every instance that triggers it, and would also be unbelievably noisy, since automatic dereferencing and automatic reborrowing are actually incredibly common.

  > pub fn main() {
  >     let mut x = String::from("moo");
  >     let y = &mut x;
  >     let z = y;
  >     println!("{:?}", y)
  > }
  > 
  > pub fn main() {
  >     let mut x = String::from("moo");
  >     let y = &mut x;
  >     let z: &mut _ = y;
  >     println!("{:?}", y)
  > }
I'd be pretty surprised if the C++ move semantics were considered more intuitive by someone not familiar with either and then learning both for the same time. Rust's semantics do depend on the type in that anything that implements Copy will be implicitly copied rather than moved, but I'm sure I understand why that's unintuitive. I'm also not sure what you mean by Rust having it's own move operator; the only thing I can think of is the `move` keyword used for indicating that closures should capture by move rather than by reference, but it's not used outside of closures as far as I'm aware, so I suspect that the confusion here is more due to expecting things to behave like C++ rather than the Rust semantics being "unintuitive" in a vacuum.

At a high level, I think the most unintuitive part of moving in C++ compared to Rust is that it can silently degrade into a copy without anything indicating it. In Rust, trying to use a value after it's been moved will give you a compiler error, at which point you can reconsider whether you do in fact want to explicitly copy or if you made a mistake or need to refactor. In C++, the only way I'm aware of to verify whether a value is actually moved or not is to use a debugger. The benefit for requiring explicit copies is similar to having bindings be immutable by default and requiring an explicit `mut` annotation; if you start out enforcing the constraint that things should be moved or immutable, fixing it later if you find out it won't work that way only requires adding `.clone()` or `mut` in one place. On the other hand, if you start out with implicit copies or mutability by default and then want to change it later, it can be a lot more work to refactor the places where the variable is used to not violate this, and it may not even be possible in some cases.

> In C++, the only way I'm aware of to verify whether a value is actually moved or not is to use a debugger.

Not as simple as if there was a proper borrow checker but its sort of possible:

https://awesomekling.github.io/Catching-use-after-move-bugs-...

Nice blog post. Nonetheless, to a new learner like me, the hardest part of rust is not its syntax; it is the ownership management. Sometimes I easily know how to implement a task efficiently in other languages but I have to fight the compiler in rust. I either need to reorganize data structures, which takes effort, or to make a compromise by cloning objects, which affects performance. Note that I do agree with rust designers that it is better to make data dependencies explicit but learning to deal with them properly is nontrivial for me.
This is exactly why I prefer Rust, eh, for everything. It forces you to think harder about your data structures and to better organize/understand your program and how data flows, gets consumed and get output.

You can ignore that in other languages, but this comes at a cost later.

My experience with Rust data structures is the opposite. To satisfy lifetime analysis inherent in pointer-based data structures, users tend to introduce additional indirection. For example, mapping keys to indexes, and then indexes to array elements, instead of simply mapping keys to values.
I've used Rust quite a bit, but I would love to hear more about this. Is this comparing to lower level languages like C/C++/Zig, or higher level GC'd languages? What problems does it make us think more about up-front, and do you think it's due to single ownership (in the C++ and Rust sense) or the borrow checker?
If you think about programming as information transformation (ie: you are getting information in, applying a certain transformation and then giving some output), programming becomes about how this information flows. Data structure becomes the most important thing.

Here is an example that now frustrates me about JavaScript. I don't know if some variables "MyVal" declared somewhere is global, local or how transfer it around. Global variables no longer makes sense to me. In Rust, global variables are possible but they are obvious and their scope is completely within your control. Now, all the data is there and it's up to you how to structure it, move it around, store it, etc.. You have to think these through before doing any code. If you don't think well enough, the Rust compiler (borrow/ownership thing) will start to give you headaches.

The short answer is... yeah, just clone the objects. Whatever other languages are doing is going to have the same tradeoff - performance (or safety, if the other languages aren't memory safe). Iff it becomes a problem, come back later and remove the '.clone()'.
You can also use Cow<> to choose between cloning and and immutable borrowing at runtime.
Unless you're using `'static` you will still run into lifetimes with Cow, which I suspect a lot of people will find difficult. I'd suggest just using clone, learning the broader language, and learning about lifetime stuff later.

Lifetimes aren't hard or complicated, what's complicated is understand their interaction with other features (closures) when you don't even understand wtf closures in rust are, or traits, etc. I'd just focus on the other stuff for as long as you can.

> Lifetimes aren't hard or complicated,

Untrue - read https://users.rust-lang.org/t/what-is-a-good-mental-model-of.... Even Rust aficionados can't describe how they work without vast screeds. Or if you think the posters there are just wrong, try the RFC on which the current implementation is based: https://rust-lang.github.io/rfcs/2094-nll.html. You truly don't find that hard or complicated?

It may be the case that people can use simplified subsets of the language to ease their way. But when a Rust advocate says that in effect to understand lifetimes you have to read the standard library source (as is written in the forum thread linked above), because no current documentation is comprehensive enough to cover it, then you know the whole thing itself is pretty complex.

> You truly don't find that hard or complicated?

This is like saying: "You think a 64bit integer is simple? OK, let's dig into the C memory model, twos complement, overflow CPU flags, how CPU caches are implemented, architectural nuances that can lead to unsynced writes from registers to RAM, etc". In reality most people can just learn that it's a number that holds 2^64 values, not complicated. If you want to really understand integers though go read the intel manual and the C memory model specification.

Are lifetimes complicated? If you're a compiler developer who needs to consider the implications across all features and edge cases, yes. If you're learning the language and you just want to write "find me a &str in another &str but don't clone it", no, it's not complicated at all.

The vast majority of beginners can just learn "& means any number of readers and no writers, &mut means one writer and no readers".

But it's so easy to get a handle on how the borrow checker works! Just check out this thread and all will become clear: https://users.rust-lang.org/t/what-is-a-good-mental-model-of...

Joke btw. That thread is a hilarious trainwreck - surely the final nail in the coffin for the Rust advocates who so often deny anything about Rust is difficult to learn.

I don't mean that as an anti-Rust jibe, in fact I'm planning to get back to it this year (having given up in despair last). I like much about it, and think it's tremendously practical for many purposes. But it just is a difficult language, no question.

I mean, some question. I learned Rust with 0 professional experience in any language (I learned it after I dropped out of a CS program after 2.5 years) and I found it pretty damn easy. That was in 2015 when the language was wayyyy less approachable (worse borrow checker, everything used nightly, smaller community, no book).

Easy is relative. I suspect a major reason I found it easy was because I didn't try to solve lifetime problems, I just cloned things. I also had primarily been using C++ in school so I was pretty familiar with pointers and, to some extent, ownership. Plus my initial foray into CS was driven by a desire to do exploit development professionally, so lower level details weren't scary at all.

> Easy is relative

In the case of programming languages, yes, it's relative to the difficulty of other PLs. I've learned many over the years, and found Rust by far the hardest (it's the only one that defeated me). And it's not the most different from others I've learned - lisps are far further from the common languages than Rust is.

> I suspect a major reason I found it easy was because I didn't try to solve lifetime problems,

Well yes anything's easy if you skip the hard bits. Learn C without using pointers.

I personally didn't find ownership & borrows the hardest part - in my case it's the great complexity of many of the commonly used libraries. Rust's complexity bleeds out into the entire ecosystem (a cultural thing).

> In the case of programming languages, yes, it's relative to the difficulty of other PLs.

I've learned many many PLs at this point. Rust was one of the easiest for me.

> Well yes anything's easy if you skip the hard bits. Learn C without using pointers.

That's my entire point. Rust is not hard if you learn the easy parts first. Trying to learn everything at once is not easy, no matter the language. Once the problem becomes just learning the borrow checker it's not that big of a deal. The harder thing to do is learning the borrow checker and traits and closures and blah blah blah.

In my opinion this is the way _not_ to learn Rust. These syntaxes are not important at all, and doesn't introduce lifetimes (which is by far the most important part of the language for deciding whether to use it or not).

Any blog about learning Rust for beginners should just contain information that helps the reader decide _whether_ she should put in the time required for learning it, then refer to the great Rust Programming Language book that's really hard to surpass.

The reference is great as well, though personally I miss a part that formally defines the type system in its current form (there are some papers about lifetimes, but they are very hard to read).

It'd be nicer if there was some way of selection which language is shown on the left side. Expecting readers to understand both C++ and Kotlin and Java and Javascript will be a stretch for most.
My experience with Java ended circa 2000 and I never wrote a single line in Kotlin. But I read these examples without noticeable issues.
what have you been programming in the past 20 years?
In approximate chronological order: Python, Lisp, C++ (ATL and Qt), Erlang, JavaScript, TypeScript, PureScript, Rust.
All of those languages adopt the C-like syntax and semantics, it shouldn't be hard for someone with familiarity with languages in that family to deduce what's being conveyed in code in languages they might not have experience with.
I thought there would be an option to select just one, but seems they are indeed just random smatterings of rust vs { Typescript, Javascript, Kotlin, Java, C, and C++ }
AFAICT, the expectation is that the reader knows at least one modern programming language from the list, and maybe is acquainted in passing with a couple of others. So at least some comparisons should click.

(They seemingly don't use more apt comparisons with OCaml and Haskell, for instance, not expecting the reader to know them.)

Javascript I've seen a decent amount and tweaked/edited some of code in it but I wouldn't say I "know" it at all. Kotlin I've never even seen before and I know literally nothing about the language. Java I wrote a bit of in high school but haven't touched it in 15 years.

So yeah there's quite a lot that people wouldn't know.

Well, Golang? Pascal? They have similar ways of declaring data types, for instance.

This guide is not going to magically teach you Rust is there's nothing to compare it to; there are other guides to help those without a background. But it could somehow help people who already have similar concepts in their mind to link them to corresponding concepts in Rust.

Golang I've seen code of, but never worked with it. Pascal I've never seen before.
You've said what languages you don't know, what languages do you know?
C, C++, and Python are all languages I've worked professionally in. I'm in the process of learning Rust.
These features aren’t each supported by all those languages though. I also don’t think expecting a dev interested in Rust to understand several C-like languages is unreasonable, at least enough ti understand these straightforward example cases.
> Inner functions. Rust also supports inner functions. ...

> Closures (lambdas). Rust supports closures (also called Lambdas, arrow functions or anonymous functions in other languages).

That's misguiding.

Closures are not lambdas. Lambdas are just syntax, but the whole point about closures is that they capture the enclosing environment (have access to variables where it's defined). Rust's documentation states just that. Closures may or may not be lambdas.

In above example of "Inner functions" (which is also a closure) that would be more clearly explained if the inner function used an outside variable. Not all languages can do that.

I keep saving these Rust resources for a near future... Am i the only one??

I really hope to start using Rust in 2023, probably for some kind of API gateway experimentation

I saved up Common Lisp resources for a few years and in 2022 I finally decided to sit down and learn it. It was entirely worth it, so I recommend you sit down to learn Rust one weekend. In fact, do it next weekend. Getting started on anything is always better done sooner than later.
joaquincabezas, don't listen to this person. It's a trap. In February I found myself in AirBnB alone with nothing to do, because my wife had to stay back home for an extra week and I waited for her to join me. AirBnB had a decent work desk, decent(-ish) monitor and barely ok keyboard so I decided to learn Rust. Now it's January, I'm at 3000+ LoC of Rust and about 4000 in Dart/Flutter and trying to make the project to ShowHN. Weekend project, my ass. Rust is highly addictive, you've been warned. I tried to get sober, left this project for months on end, but always relapsed.
fn main() { println!("Too late I guess!"); }
> Getting started on anything is always better done sooner than later.

So true. It’s never too late, you’re never too old, there’s never something else you need to learn first, just do it.

Nice. I'm doing exactly that with CL, one of my new year's resolutions. Would you mind to share your resources?
Besides the obvious Big 4 (Gentle Intro, PAIP, On Lisp, and Cookbook) I am also quite fond of Lisp in Small Pieces. Aside from those I'd also recommend grabbing the GCL source and building the Info manual so you can browse it in Emacs, it contains a fairly complete copy of the HyperSpec (I know it's available online, but I don't like visiting websites that haven't updated to HTTPS yet).

Also check out CLiki (the Common Lisp wiki, https://www.cliki.net/), it's very helpful in finding useful libraries, like Alexandria, defstar, trivia, lparallel, and so on.

Didn't know half of those. Thank you!
I can only justify Rust for hobby coding, none of the stuff I do professionaly cares about what Rust offers, compiled managed languages are good enough and have decades of software maturity, and using Rust as translation layer between them and C++ libraries hinders more than it helps.
Yeah I wish there was a strong jobs market for Rust developers then I'd feel like I wasn't wasting my time.
There aren't many full Rust jobs but there are a whole lot of companies with ever expanding bits of Rust in production. Probably the best way at this point is to push it internally and put together a convincing case for using Rust for new projects.
This is our company. Frankly, I don't agree with this approach. We're a hedge fund that uses Java for low latency trading systems. Someone pushed for Rust to be used for a new project, because they wanted to use it, and now we have a split codebase. We have duplicate code (eg. connections to external APIs) written in Java and Rust, and now we have two sets of code to maintain whenever one of those APIs change.

I always get annoyed when I hear about people suggesting to push for a language in a company because they feel it would be a good fit. Sure, maybe it would be, but now the company will always require experts not in one language but two, and developers are no longer as fungible between teams.

Having said that, from a selfish perspective, I will use it to write some production Rust code in the future.

What kind of Rust job would you be looking for?
This document only briefly mentions interior mutability, which IMO, is one of the most important things to become productive in Rust.
Curious why the author chose not to discuss macros? You encounter them immediately with a Hello World.
Declarative macros would be too confusing to get into in any way other than "the ! in println! denotes a macro".
This is an excellent short tutorial. It helps to compare and contrast to other languages.
As someone coming from a Java background, this seems useful! Thank you!
any constructive criticism on rust syntax?
With the caveat that syntax is the ultimate bikeshed topic, one (IMO) syntactic wart is the special pattern syntax:

1. "Operators" have different meanings. `1 | 2` and `1..=2` mean something different in patterns than in expressions. Here is a silly example: https://rust.godbolt.org/z/76Ynrs71G

2. Ambiguity around when bindings are introduced. Notice how changing a `const` to a `let` breaks the function: https://rust.godbolt.org/z/aKchMjTYW

3. Can't use constant expressions, only literals. Here's an example of something I expect should work, but does not: https://rust.godbolt.org/z/7GKE73djP

I wish the pattern syntax did not overlap with expression syntax.

> 2.

This actually bit me in the a__ when I misspelled my enum variant and instead match created a variable named like that, that captured everything and I got only a warning and very wrong code.

I think there should be `let`s inside match block if matching creates some variables.

I don't like how

fn func<'a>() means that 'a must outlive execution time of func

but

T:'a means that references contained in T must live longer than 'a

and

'a:'b means 'a must live longer than 'b (that's consistent at least)

Maybe:

    fn 'a:func() {
or

    fn func() 'a:{
would be better for indicating that 'a should outlive function execution.

Maybe some directional character would be better than : (> is probably out of question because of <> generics)

----

I feel like structs that don't have 'static lifetimes because they contain some borrows should have it indicated in their name.

For example:

    struct Handle&<'a> { n:&'a Node }
or even

    struct Handle&'a { n:&'a Node }
or

    struct Handle& 'a:{ n:&'a Node }
to indicate that 'a must outlive the struct.

Then you could use it:

    let h = Handle& { n: &some_node };
Maybe functions that create non-static struct might have & at the ends in their names.

Like

    vec![].into_iter().map()
but

    vec![].iter&().map()
    
You could easily see that you are dealing with something you should treat like a borrow because it contains borrows. Such structs would be sort of named, complex borrow and raw '&' borrow would be anonymous or naked borrow.

Not sure if it would also be nice to differentiate structs with &mut

----

I would just like to separate lifetimes syntax from generics syntax because those two things have nothing to do with each other from the point of view of the user of the language.

----

I would also like to have

while cond {} else {}

where else is only entered if cond was false from the start. But that's a wish not specific to Rust.

Using clearly defined bit sizes (i32, f64) rather than legacy naming conventions (int, double) is a good idea, the language could be really quite something if they switch to S-expressions.
Syntax or semantics? Not a lot for syntax... maybe the "turbofish" syntax with generic types is a bit too much line noise: <Foo<_> as Bar<_>>::baz<_>()
Definitely, rust looks clear for what is programmatically happening, making it noisier to overview the problem being solved
the hardest part and barrier are the concepts behind lifetimes/ownership/borrowing not the syntax
I think the hard part is understanding how limited is basic feature set of just Rust.

That you can write very few interesting programs without venturing into the heap with Box, Rc and such and into internal mutability with Cell and RefCell.

Then it quickly raises to the power of other languages and surpasses them with "pay for only what you use" mentality.