Hacker News new | ask | show | jobs
by atoav 2674 days ago
This is in tune with my own experience using Rust in production: it can stop you from doing certain classes of mistakes, but it won't stop you from doing stupid things.

But the idea that I don't have to think about certain classes of problems allows me to give these stupid things more focus, which is surprisingly refreshing.

The predictable nature of Rust was so refreshing for me that I ended up using it even for smaller reusable scripts where I would happily have used Python before but soon got annoyed with obvious errors that would only show up once you run a program.

If you e.g. have a `print foo` in some obscure branch that rarely happens, that print will ruin your day if you use Python 3. If python would be a little like Rust you would get on save (or at least on compile) a hint or error, that the print should look like this: `print(foo)` for Python 3. You can be incredibly careful and rust will still catch things now and then, that would have gone unnoticed into production unless you have immense test coverage.

I like Rust for the experience I had with it. It definitly changed how I approach certain problems in a very good and productive way, even when I don't use it.

8 comments

Agree completely. For a bit of my own story, a year+ ago I had the option to write a project in Rust and evaluated it vs Go. Long story short, I tried rust, and it was a massive headache and I failed. We used Go (as I had been for ~4 years).

Fast forward to ~2 months ago, a work project dictated tight control over memory which, while possible in Go, had me looking at alternatives. I decided to give Rust another try. This time it wasn't just an evaluation, it was needed to work so I bought and Rust book and spent some after hours time learning/etc.

This time, Rust has been an absolute joy. I have no understanding why last time was so painful, and this time it's been so amazing. Maybe it was the book[1]? Maybe it was just a 2nd round of learning based on my previous experience? Regardless, it's been great.

There's just so many mental overheads like what is concurrent safe, what is non-null, etc that are just great to not think about anymore. On top of that, the formatter and LSP are just great. It highlights in my text editor (Kakoune) what variable caused an error, where it gets moved incorrectly, etc. So much just works, it's great.

My only complaint these days is:

1. I find it odd that some things like slice reads can still panic by default. Yes, I can use `foo.get(1)` to avoid panics, but still - it's a bit odd to me. 2. I'm anxiously awaiting async/await. It's quite difficult to be patient.

[1]: Programming Rust: Fast, Safe Systems Development

> I find it odd that some things like slice reads can still panic by default. Yes, I can use `foo.get(1)` to avoid panics, but still - it's a bit odd to me.

Panicking is a perfectly good way to handle the situation where an invariant of the program is violated. That is, it is perfectly fine to index using [] in cases where there cannot possibly be an out-of-bound access unless the program has a logic bug. And if it does have a bug, there's usually not much that you can do except abort because you can't trust the program anymore.

On the other hand, if you know that your input might be OOB without violating a precondition, as a part of the normal execution of the program, and you can handle it gracefully, use `get` instead.

This distinction between "expected" and "unexpected" errors is an extremely valuable one, and one unnecessarily muddled by languages where all errors are signaled with exceptions. Rust gets it right.

I don't understand, tbh. Eg does Rust not make great effort to prevent null pointer exceptions? OOB feels quite similar to null pointer.

It's strange to me because a lot of what I value in Rust is writing code that handles all outcomes safely and with confidence. Yet that all seems to be out the window if you access array values - as it may or may not work.

The safety of [] slice/array access is entirely up to how the dev wrote the code.. which largely feels the same as any other language.

Am I thinking about something incorrectly?

The usage pattern is somewhat different. The primary problem with null references in languages such as Java is that they are not opt-in, and they're not even opt-out! That is to say, you don't even have an option (heh!) to encode in the type system the fact that a reference is never null. What's worse, references tend to get passed around a lot, and stored inside objects, and programmers are lazy and don't assert or even think about preconditions, so the root cause of an unexpected NPE can be far removed from the code that finally triggers it.

With code that indexes slices, the index computation is usually much more proximate to the actual indexing operation. If ensuring the validity of the index is about upholding an invariant internal to the module, it makes no sense to return an "sorry, I have a bug" error to the caller if that invariant fails to hold. What would the caller do about that?

I think the slightly odd or inconsistent thing is that rust has [] at all. I think of it as a compromise for people's familiarity with the operator. And if that's all it is, it makes sense that it behaves the way it does; I think a panic is more similar to what other languages with that operator do with an out of bound index than returning Option would be.
It’s probably a combination of both things, but “I tried rust, struggled, quit, came back months or years later and now I have no idea why I thought it was so hard” is certainly a recurring pattern we’ve seen. Glad it’s working for you now!
Both type inference and borrow checking have also received a whole bunch of small improvements over the past couple years. Subtle enough that you probably wouldn't notice them except in that the language just takes less effort to use.

If you use Rust 2018 now, non-lexical lifetimes are a bigger recent improvement: https://doc.rust-lang.org/nightly/edition-guide/rust-2018/ow...

Also error messages have had a lot of effort put into them, so an error that would have been confusing a few years ago, now may no longer be confusing.
This is true, but it also happened a lot even before NLL. That said, I do think it's an additional factor today, you're right.
Because people underestimate the amount of effort needed to learn a new language at first?
For real Beginners to programming Rust can be hard, because concepts like pointers are hard in most languages that use them.

For Programmers that try Rust out Rust can be hard because they are stubornly trying to program Rust as if it were Python/Java/C/C++/Foo – but it isn't.

Rust as a language makes certain types of approaches (or anti patterns) nearly impossible. In the beginning it happens quite often that after a day of struggle you will erase a Rube-Goldberg-Machine that does something simple and replace it with the right line of code that does exactly the same while beeing way more extensible.

If you find yourself building more and more elaborate structures just to get something very simple done, the problem is very likely your approach (e.g. trying to implement a OOP structure instead of solving the problem) – at least that is what happend to me a lot.

Rust's borrowing and ownership concepts, as well as traits and generics make it extremely powerful, but these concepts lend themselves better to certain ways of structuring code than to others. Learning something is in my opinion always about allowing something to change your perspective. If you are not allowing the thing you are learning to change you, chances are that you are not learning, but judging.

From my friends that have tried rust, creating a graph or doubly linked list without using some special structures is fairly painful or impossible. You can create a graph by using some sort of adjacency matrix or some other kind of structure that keeps ownership in some sort of tree without much pain, but that has it's own downsides.
This was also my experience. It's a fine language if you can't (or more usually "won't" for spurious reasons) use a garbage collector. However a lot of things are just easier to do using a GC, and I suspect in many, not all, of the use cases of Rust, a GC would be just fine.

(Also I have to rant here a bit: Yes I know the GC you used in Java or Emacs in 1997 was terrible, but modern GCs are very good indeed)

...annnnd this is probably a good example of the frustration that comes from solving familiar problems in a new paradigm, before the lightbulb clicks on.

Disclaimers: I'm only lightly familiar with Rust, and linked-lists are perfectly reasonable solutions _in the proper context and paradigm_.

So the problem to be solved is that we want to a) have collection of items which have a very strict ordering; b) be able to directly access the first and last item in that order; c) given a particular item, find the item which is immediately before/after it; d) when removing or adding an item, the relative ordering of the other items is undisturbed. Oh yeah, and e) we want it simple and efficient.

Doubly-linked lists are, of course, a very common solution used for these kinds of problems. They are perfectly suitable _if our paradigm is_ "I hereby assert that somehow, external to the compiler, I've verified that all list manipulation is being done correctly in all circumstances." If, however, our paradigm is "I want the compiler to automatically guarentee lots of these invariants" we run into some problems:

1) The data structure itself (each item has two pointers, and there are two global pointers for "head" and "tail") makes very few guarentees. Just one, actually: "each pointer will either point to an object, or will be null (or its moral equivalent)". There just isn't much compile-time meat for the compiler to chew on.

2) Linked-lists typically have (relatively) persistent scope, and exist outside of the lexical scope of whatever code block is immediately operating upon them. Again, it doesn't give the compiler much to go on.

3) Without managed memory (GC), there's no way for the compiler to guarentee that a pointed-to object still exists.

4) There's no built-in guarentee that the "head" and "tail" pointers actually point to the first/last object.

5) There's no guarentee of overall ordering (if a.next == b, then b.prev == a).

6) There's no guarentee of even a consistent view of the items in the collection (head == a, a.next == b, but b.prev == null/end-of-list).

7) There's no way for the compiler to guarentee, when viewing the collection as a whole, that modifications are atomic or thread-safe.

Yes, it's possible to take care of these issues in a non-Rust paradigm by making the structure of the list itself opaque, and only exposing insert/remove functions which enforce all the invariants. (I think Rust itself can do this with unsafe code.) However, you're still left with difficulties:

8) If list items are opaque containers, how do you guarentee that the payload object continues to exist?

9) How do you guarentee payload object ownership, atomicity, mutability, or other properties?

10) How do you guarentee that these needed invariants are held transitively?

Now the Rust paradigm, of course, isn't the only way to deal with these issues. But whether you're using Rust, managed-memory, C++ smart pointers, immutability guarentees, etc., you're going to need to do things that are unnatural in the other paradigms. Rust has the benefit of automatically enforcing lots of these invariants without having to do copying, worrying about shallow vs. deep copying, transitivey, etc.

Have not used rust but I'm assuming it's because when you're down a rat hole rust stops you cold at every turn. You need to back up and rethink what you're trying to do. But when you don't have a good handle on what rust is complaining about you can't see that.
The books are incredibly well written. When people are “stopped cold at every turn” they are usually programmers who come to Rust with a metric ton of existing assumptions how to solve certain problems, without really thinking about the concepts of the language itself.

When a C++ programmer starts with python they will write incedibly “unpythonic” code, when they do the same thing in Rust, it just won’t compile.

IMO Rust is very straightforward and the std library is incredibly good. But you have to really understand certain core concepts and what they mean in terms of structuring your code.

Just like that C++ programmer needed to learn what is ideomatic python looks like and why it makes a lot more sense to write python that way, you need to find the rust way of things. But this is the same for every progtamming language out there..

I agree entirely.

One of the reasons why when I learn a new language I deliberately research the idiomatic way to write that language, it saves a bunch of trying to hammer a square peg into a round hole time.

Also some languages purely because of the domain they are in can do things in ways that others can't (trivially) do so if you treat each language as "foo" with different syntax you end up hobbling yourself to the lowest common denominator of "foo"'s features across every language you use.

You've restated the point I was trying to make much better than I did.
I'm not sure it's that simple. I've never heard anyone say this about Go, for example.
To be fair, Go is uniquely designed to be easy to learn and become productive with quickly. It was practically a design goal of the language.
Sure. I think it's one of Go's strongest points. But it's still a counter-example to the parent's point. Learning a new language does not inherently require a lot of effort, and so expecting it to be reasonably easy, especially when you're an experienced developer, doesn't seem unreasonable to me.
There's plenty of languages which take limited / little efforts to learn. So I'd say more that the unfamiliar concepts need to stew or sink in.

One way is to just keep at it until it clicks. An other way is to not do that, but once you've got the words for something you start seeing the issue everywhere, and next time around it makes a lot more sense.

> I have no understanding why last time was so painful, and this time it's been so amazing.

It has been my experience that if I try to learn something hard and give up and then come back to it a few months later, I have a way easier time even with no apparent changes in process. I suspect some of it is giving your brain time to process fundamentals or something.

This seems true and unsurprising - it strikes me as being similar (or identical) to spaced repetition, which is a powerful learning technique.
> 1. I find it odd that some things like slice reads can still panic by default. Yes, I can use `foo.get(1)` to avoid panics, but still - it's a bit odd to me.

I wonder if this is similar to C++'s `[]` vs `at`. `at` does implicit bounds checking but, as an optimization, if you are already doing an explicit bounds check, you can elide the implicit check via `[]`.

It's more about ergonomics. With `get` you get an `Option` which allows you to handle the out-of-bounds situation, but is unnecessarily noisy when you know that the index must be valid unless the program is buggy (and in that case you most likely want to abort anyway).
It's somewhat similar, except C++'s [] leads to memory unsafety whereas Rust's [] on slices has bounds checks and will abort the program on failure. Your program will crash but it won't be an exploitable bug (except for a DOS). .get(), meanwhile, returns an Option so that code written using it can recover from OOB accesses.
Except Rust's [] behaves like C++'s at (checks and aborts). C++'s [] is called `get_unchecked`.
C++ at() doesn't check and abort, it checks and throws a specific documented exception that you can catch. So it is, in fact, closer to get(), just using a very inefficient way of reporting.
> C++ at() doesn't check and abort, it checks and throws a specific documented exception that you can catch. So it is, in fact, closer to get(), just using a very inefficient way of reporting.

You can catch a panic, and you can compile C++ with -fno-exception. at() is not closer to get() than to [].

If you do that, you will be invoking nasal daemons, as at() is required by ISO C++ to throw.
I wonder if it has something to do with the nature of the problems. Rust can be very smooth or a real pain, depending on the data structures you need to model - basically, whenever ownership is unclear and sharing is common, it's going to be more painful.
Usually that just means you have to think much harder about the problem you are trying to solve within the new set of requirements (borrow checker, lifetimes).

Weirdly enough these things should also be a hard problem in other non-Garbagecollected languages, but you get away with a lot more when the compiler doesn’t force you.

From a purely practical level there is so much already that I had a hard time finding some data type that had no existing and tested solution for it. Looking at the code in the std library can be enlightening.

> Weirdly enough these things should also be a hard problem in other non-Garbagecollected languages, but you get away with a lot more when the compiler doesn’t force you.

It's the same as with static vs dynamic typing - some things are just easier to model with dynamic, because you don't have to fight with the type checker to prove that your program really is sound when you know that it is. But, of course, you might believe it to be sound when it really isn't...

How do you like Kakoune? It's the first I've heard of it
Love it - was a Vim user for many years before that. I will say that while the Unix-y design is really cool, the plugin system written around Unix (Bash) is a bit of a pain. You technically don't need to use bash at all, but idiomatic plugins tend too.. so.. meh.

These days I'm wanting out of the Terminal - but wherever I go, I'll need to take Kakoune's style of modal navigation with me. I love it too much.

_(For reference, I'm hoping Xi editor implements the foundation of modal editing, and then I can make Kakoune style editing work in Xi Editor)_

> before but soon got annoyed with obvious errors that would only show up once you run a program.

From what I gather you are comparing static vs dynamic typing, and I agree. I do not use Python because I prefer a subset of errors caught at compile-time. However, it is silly to make it sound like that this is somehow limited to Rust. You could just as well have used Go or OCaml and feel "refreshed" because obvious errors would have been caught at compile-time.

> The predictable nature of Rust

Again, given the context, it is static vs dynamic typing. This is not something limited to Rust. In case you were not talking about static typing and its pros, could you please tell me what kind of official formal proof tools exist for Rust that makes it predictable?

Sorry if I angered you. I never claimed that Rust was the only language that has these properties.

My experience was is: I have never seen a language/environment where these rules were so well thought out, so well communicated and so well enforced. The package managment works like a charm, the module system (since edition 2018) is the best I have seen etc.

These are all subjective – I am a film/philosophy student who codes and I never received any formal education on any CS related topic. I was speaking about why Rust feels sound to me. Good chances Haskell will look sound to me too, very good chance I might check out more CS topics. No hard feelings please.

It is fine, and no it didn't anger me, so no need to apologize, really. :)

> I never claimed that Rust was the only language that has these properties.

Yeah, that is my mistake. Sorry for misunderstanding you!

> I never received any formal education on any CS related topic

Same here actually! :P

> No hard feelings please.

Not at all!

I understand that you like coding in Rust, and that is completely okay with me, and even if it was not, that should not deter you from continuing what you are doing.

>could you please tell me what kind of official formal proof tools exist for Rust that makes it predictable?

I feel like you aren't really asking a question, that your intent was really to lecture someone, but in case you really are:

* a borrow checker * option types

So OCaml would catch a similar set of obvious errors, though Rust would also catch all data races at compile time, as the borrow checker does that as well as prevent memory mistakes.

No, I was really curious. My bad if it looked like I was trying to lecture. I do not think that I have the necessary knowledge in this topic to do that. :)

> a borrow checker

What exactly do you mean? How does it differ from any other language's type system?

> would also catch all data races at compile time

In Ada/SPARK, you can formally verify tasks, too. Please take a look at https://docs.adacore.com/spark2014-docs/html/ug/en/source/co... if you have some time!

> prevent memory mistakes.

Which mistakes are you referring to specifically? I need to know so I can have a meaningful response to it, but all I can say right now is that Ada/SPARK does the same.

> > a borrow checker

> What exactly do you mean? How does it differ from any other language's type system?

Part of rust's type system is sub-structural: by default, Rust's types are affine, which means you can only use them once (at most, not exactly).

Now this is not super convenient to use and could have efficiency issues (e.g. any time you want to check a value in a structure you'd have to return the structure so the caller can get it back), so to complement this you can borrow (create a reference). The borrow checker is the bit which checks that borrows satisfy a bunch of safety rules, mostly that a borrow can't outlive its source, and that you can have a single mutable borrow or any number of immutable borrows concurrently.

The borrow checker provides for memory-safe pointers to or into a structure you're not the owner of, with no runtime cost.

> What exactly do you mean? How does it differ from any other language's type system?

If you're familiar with affine types, this is basically what Rust's borrow checker implements.

If you're not familiar, it's a bit complicated to summarize on Hacker News. You should try it out, because it lets you guarantee statically entire classes of properties that non-academic languages struggle with.

> In Ada/SPARK, you can formally verify tasks, too. Please take a look at https://docs.adacore.com/spark2014-docs/html/ug/en/source/co.... if you have some time!

I'm not an expert in Ada, but yes, that's pretty similar to some of the properties Rust will let you check out of the box.

edit My memory of Ada was incorrect.

It's more than just affine types, it's region typing, which is far less common.
Fair enough.
Can you recommend a good book on SPARK in general? I've read one on Ada 2012 itself, and what it had to say about Ravenscar and SPARK got me interested.
"Building High Integrity Applications with SPARK"

There is also "High Integrity Software: The SPARK Approach to Safety and Security", I never read it, but it is written by a well known author in the Ada community.

That’s one aspect of the errors that are avoided, but there are many others too - some of which aren’t solved by other languages. Go, for example, will happily nail through all of your obviously null pointers with gay abandon. The Rust ownership model prevents other classes of bugs.

It’s not a panacea—and to be blunt, I find Rust more work than it is worth for the sort of projects I typically work on. But any tools which can eliminate whole error categories are worth looking at for sure!

I was responding to the issue I quoted, which really is just a matter of using a programming language with static typing. :)

> some of which aren’t solved by other languages

You only mentioned null pointers, so I will go with that. In Ada, you can have access types (pointers) that are guaranteed to not be null, and accessibility rules of Ada prevent dangling references to declared objects or data that no longer exists, so this particular issue is solved by a language other than Rust. Please feel free to give me other examples of errors or issues that you may believe is not solved by languages other than Rust.

https://www.adaic.org/resources/add_content/standards/05rat/...

> But any tools which can eliminate whole error categories are worth looking at for sure!

I agree. That is why I think Ada/SPARK is awesome! :P

I will give you a few examples. My knowledge of Ada is insufficient, so I'll let you tell me whether Ada can solve that without using an external prover (note: being able to use an external prover is great and I haven't seen this done in Rust yet :) – but that's not the topic at hand).

1/ Consider a file `f` (or a socket, etc.). Using the standard library, Rust will statically ensure that, once the file is closed, you cannot attempt to, say, read from it. This is nothing special to files, just an aspect of the borrow checker.

2/ Consider a communication protocol. You need to send a message `HLO`, expect a message `ACK`, then send something else, etc. It is pretty easy to design your objects such that the operations of sending the message, receiving the message, etc. will change the type of your protocol object, ensuring statically that you never send/expect a message that you're not supposed to send in the current state.

If you're curious, I wrote a blurb last year on the topic: https://yoric.github.io/post/rust-typestate/

3/ I quickly googled "Ada spark phantom types" and didn't find anything. Does Ada support phantom types?

SPARK is a subset of Ada 2012.

Your examples are possible with contracts.

I'm interested, do you have examples somewhere on how to implement that kind of properties with contracts?
Ada is awesome, and if compiler writers had been more timely it might have taken hold. Rust solves the same problems without a runtime, and about 3 times the performance. Granted it looks like c++ and ocaml made a particularly ugly child.
>> [...] solves the same problems without a runtime, and about 3 times the performance.

I think you should look more into Ada. The only "runtime" is for the exception handling and bounds checking, both of which can be turned off if needed.

And I don't know where you got that "3 times" figure from? Do you have an example you are referring to?

I knew you could turn off the runtime but not about the formal verification lint tools that make it safe to do so. My reference for performance is the benchmark game. If you or someone else can mod those programs to beat rust I'd be thrilled. I like Ada.
Which problems are you referring to?

You can do everything that has been mentioned without Ada's RTS, you can also disable all run-time checks. You can use static analysis tools (there are many, and available for free), for example, you can formally verify the correctness of the program, no run-time checks required.

This is not only static vs dynamic typing. Rust provides a set of abstractions seldom found elsewhere. The borrow checker, for instance.

It is more a question of semantics.

I assumed borrowing and lifetimes were part of his experience as well.
Sure, and I find both not very hard as a concept.

The hard thing is not understanding the concepts but learning what this implies for your coding style, especially when you come from a garbage collected language like Python.

Both concept can at times give you hard to solve tasks, but it usually is because the problem you are trying to solve is hard and you cannot just naively hack away without thinking long and hard about it.

Other than that I learned liking the borrow checker and lifetimes because you will get a incredible narrow scope of which variables can matter when — which makes debugging much easier

And with something like Mypy, you can get most of the benefits of static typing even in Python, if you want to. But Rust has so much more to offer, such as memory ownership.
I didn’t know mypy, but I am going to check it out, thanks!
Python will actually give you an error for `print foo` as soon as it parses the file. But there definitely are other scenarios where you'll only get the error in the middle of execution (such as passing the wrong type of thing as an argument to a function)
Agreed but Python lazy-loads & parses files as packages are imported. By convention these are all at file scope and at the top of the files, so it's often confined to an initialization phase. But...sometimes developers use clever fallback behavior by catching ImportError.

So the scenario described is possible to escape simple tests, I suppose.

Doesn’t the print raise a SyntaxError not ImportError?
Yes, sorry if that was confusing -- perhaps I shouldn't have brought up ImportError. The point was that uncovering SyntaxError could possibly be as challenging as other feature tests.

    try:
        import collections
    except ImportError:
        # fallback impl of collections:
        import something_that_uses_print_statement
Use a linter. Most IDEs will do it for you natively, and even if you're dealing with a terminal based editor like vim, it's really easy to get the output of pylint, flake8 etc. running in it. It's extremely rare I ever get a syntax error make it through to code review, let alone build/deploy.
Linters catch the easy cases but when the problem spans multiple files they have no clue what is going on.
The mypy type linter performs type checking across files, in many cases even without explicit annotations.
I recommend using mypy for python, type safe python has came a long way. The type annotation syntax has been in the language spec for years
> If you e.g. have a `print foo` in some obscure branch that rarely happens, that print will ruin your day if you use Python 3

You can use any compiled language for that though, and even some uncompiled ones (PHP will refuse to run the file)

A linter would catch that.
Every software allows you to do stupid things. Its your job to avoid them.