Hacker News new | past | comments | ask | show | jobs | submit login

Going from C++ to Rust, keep in mind that Rust does not mainly solve a technical problem, but a human one. It's not so much about making programs that run fast and don't fail than about making such code writable and maintainable by a wide variety of people over a long period of time. The initial Rust learning curve is steep but the competence curve is actually much flatter than C++, IMO. It's worth reiterating this because this is not as apparent to a seasoned systems programmer.



I do find that surprising. I have never used rust for more than toy programs, and I don't use a fraction of what is available, how can a language with so many features have a flat competency curve? I can easily write something that doesn't use traits or iterators, panics all the time, and calls clone all over the place to satiate the BC and call it a day right? I could create very unidiomatic code that was still correct, but not performant or easy to read/use. I suppose the fact I was able to make that list without ever having used the language in anger maybe makes your point... But with all those features surely I'm missing as many pitfalls as I know about?


I agree with your parent's assessment. And I think it may be because Rust's multi-paradigm approach allows you to use what you need when you need it.

> I can easily write something that doesn't use traits or iterators, panics all the time, and calls clone all over the place to satiate the BC and call it a day right?

You're basically not going to write your own traits until you've spent the time required to get something working. You'll copy and paste and modify to create another function.

If you're unfamiliar with iterators, then you'll write for loops. I was unfamiliar with iterators until I said, "I'm going to rewrite every for loop in this personal project as an iterator." Rust forces you to use lots of patterns re: memory, but forcing you locally to implement as a for loop or a iterator is not one of them.

I think a fear of clone() is wrongheaded. Most of the time it just doesn't matter.

When you think, ugh, this isn't the best I can do, it's time to learn iterators/traits, then you can modify the code to work then.

> But with all those features surely I'm missing as many pitfalls as I know about?

I guess my question would be: compared to what? I think the pitfalls are much, much, much more explicit in Rust, than C, like this won't even compile because this code has a condition which might be a null which I have not handled.

For app development, Rust really feels like a smallish language once you get over the hump.


>I can easily write something that doesn't use traits or iterators

Do you mean that you aren't using them at all or you aren't defining your own? Because I would expect any nontrivial rust program (outside of some obscure embedded context maybe) to make use of the traits that are in the standard library/your dependencies. Similarly, you can't even write a for loop without using iterators.

>panics all the time

That might be fine, depending on your usecase. Presumably if you were going to put it into production you would go through and handle the errors. Crucially, by grepping for `unwrap` and `expect` you can find the panics very easily.

>calls clone all over the place to satiate the BC

If it's not a performance issue then this might be fine.

>I could create very unidiomatic code that was still correct, but not performant or easy to read/use

This is true in every language. The value proposition of rust is that if you do write idiomatic code then it will likely be reasonably performant and easy to maintain.

>But with all those features surely I'm missing as many pitfalls as I know about?

Maybe, but not every program will need to use every feature. Quite a few of the more advanced features are mainly useful for libraries, for example.


> Crucially, by grepping for `unwrap` and `expect` you can find the panics very easily.

I write Rust code up and down the stack, and this is the thing that ticks me off more than anything else. You can get most of them looking for this stuff (and a handful of others), but there are just so many places in vanilla Rust that exhibit the Wrong Behavior By Default, e.g.

    a[i] = x + 1;
Which looks so innocent but can panic in two places! Ugh.


If x's Add doesn't have panic on overflow then you get rid of the panic risk on the right hand side.

For example if x is Wrapping<u8> then it can't panic. Having chosen Wrapping<u8> you will presumably not be startled when 255 + 1 == 0

Or if x is Saturating<i16> again, no panic, but 32767 + 1 == 32767

Wrapping and Saturating really exist (well Saturating isn't stable, but it's in nightly) and if you're in a space where that's what you want, you can express it cleanly in Rust. Most programmers just mean the integers they learned in school which don't have a "maximum" or "minimum" they go on forever, and so overflow means the programmer's model of the world mapping those integers to a type like usize was wrong and this code is faulty.

[Edited to add: This doesn't quite work because in the end neither Wrapping nor Saturating implement Add over the normal integers, since it's not certain what you intend if you wrote that, better to have an error asking you to explain. AddAssign works though]

Now, there aren't any built in types whose IndexMut can't panic AFAIK, but we could build one, and if a was such a type then a[i] doesn't panic either.

For example we can imagine a is a (hypothetical) PythonDict while x is a Saturating<i16> and now our code just compiles to add 1 to x, saturating if necessary, and then shove the result in our PythonDict under the value of i.


Yes, Wrapping and Saturating very neatly provide behavior you might want in some context. I use them a lot in embedded contexts.

Since indexing is nearly always fallible, it should be fallible!


Rust's slices (the most obvious thing you would index) have three ways to index them, if you are out of bounds the consequences are Undefined Behaviour; panic; and return None. Today those are provided by get_unchecked(), the Index operator and get() respectively. [[ Calling get_unchecked() is unsafe of course, but it's potentially faster because it has no bounds check ]]

I'm confident we sometimes want each of the three options, so if we're having the Index operator return None then get() presumably panics, or at least some new function is provided with that behaviour.

I reckon this just drives annoyance that [k] has to be unwrapped, while get(k) does what people actually wanted [k] to do. I could be wrong, but that's my sense.


I don't think I agree that we need three different things to handle them. We already have a general mechanism to convert an Option<T> to a T and panic on None, a method which shall not be named. So we just need get and get_unchecked, really.

If the index operator is offered as sugar over one of the basic operations, great. But why add sugar for a method call that we actually don't ever want to see in production code?


There are two clippy lints that cover these possible panics:

- indexing_slicing

- integer_arithmetic

Enabling those will throw an error on usage in your codebase.

Also it's worth noting that arithmetic is only checked against overflow when building in debug mode by default.


> Also it's worth noting that arithmetic is only checked against overflow when building in debug mode by default.

Yes, the default behavior in release mode is even worse!


One can conceive of tooling that can find these panics for you so you don't need to grep, but I'm not aware of anything that's been written at this time.


Sure, you wrote 2 overflows - a buffer one and an integer one. Would you rather it silently breaks?


No, I'd rather it not compile!


I meant "flatter" as in "linearly increasing" competence. Once one knows the basics, it's easy to write "correct" programs in Rust that will not exhibit unexpected behavior. Of course, learning more of the language, one will write more expressive and performant constructs. The progression is made easier by the language and lib having relatively few pitfalls. Also, much care has been given to the developer's cognitive load through proper tooling (cargo) and informative error messages from the compiler.

"Fighting the borrow checker" has become a Rust meme but it's actually a good part of the Rust experience to have a "conversation" with the compiler. One is forced to reflect upon the implications of the app or lib's model, often leading to a better design or at least an understanding of the limits of the code. This is possibly similar to what LISP people describe when talking about their REPL-based workflow.




Join us for AI Startup School this June 16-17 in San Francisco!

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

Search: