Hacker Read top | best | new | newcomments | leaders | about | bookmarklet login

> Just making borrow checker violations into warnings for debug builds would go a long way to making the language more tolerable to the overwhelming majority of programmers not already committed to it.

The problem is that the borrow checker isn't just a verifier of correctness. It actually impacts the semantics of a program.

For example, in javascript if you open a TCP connection and let your connection handle go out of scope, the connection will stay open. And as a result, your program will sort of just hang. In Rust, when you open a connection and your handle goes out of scope, the socket is closed automatically. How does the compiler know when your program should close the socket? It knows based on the borrow checker's analysis of your program.

If your program confuses the borrow checker, that means that the compiler can't figure out when it should close that TCP connection on your behalf. This isn't the sort of thing you can just turn off in debug mode, because programs should act the same in debug and release modes. A program which doesn't close the TCP connection is a different program from one which does.

There are ways around this - you can use types like Box, String, Rc, Cell, etc. And then just .clone() all over the place. But your code will run slowly. And thats still way more annoying to use compared to other languages. I don't know any other language which makes you write "Box" to put an object on the heap.

I hear the criticism. It is legitimately really hard to learn how to write rust code in a way that makes the borrow checker happy, especially at first. I'm still avoiding async rust after getting my feet burned by it a couple of years ago. As another commentator has mentioned, rust makes you experience all the pain up front. Its a lot of pain for new programmers to the language - probably too much.

I genuinely believe rust will never achieve the popularity of languages like Python, Go or Javascript. Its just so much harder to learn. I don't think there's anything wrong with that. Most programmers don't need to write systems software. I'd rather rust keeps focuses hard on what its good at: situations where performance and correctness are more important than development speed. We have plenty of good programming languages for quickly prototyping things out. There's nothing wrong with using them when they're the right tools.



view as:

A buggy program is a buggy program.

But sometimes you have other bugs you know about that you want to fix first. Or you want to try out something else, first. As long as your builds report violations, you will know you aren't done. You might suspect one or other warning is why something mysteriously doesn't work, and try tackling that. Or you know already, and have bigger fish to fry.

Before you are done, and ship, you will certainly have fixed everything, because your release build had to complete without violations.


The point isn't that there is a bug, it's that you haven't told the compiler enough information about what to do. It's kind of like writing a generic function signature and asking it to fill in the blank when there are many ways that can be done all with different semantics.

Haskell has type holes which cause a runtime error when executed as they cannot be evaluated which is an interesting approach (it also has other uses, such as figuring out types).


Exactly. The borrow "checker" is misnamed. Its part of the compiler. You can't skip borrowck and still produce a working executable for the same reason you can't skip linking.

Hm. Or can you? It might be possible to make a special "leaky" rust compiler mode where:

- Your program is only allowed to be single-threaded

- Nothing ever gets dropped. (All allocated memory leaks). And all variables get put on the heap.

- Mutable references can alias

That might be enough to make rust's lifetimes irrelevant. But I'm not convinced the juice is worth the squeeze.

If you don't mind segfaults, you could make it useful by copying C's semantics instead - but you'd need to manually free memory to avoid memory leaks. Or if you also don't care about performance and rust's ffi, you could put a tracing GC in there. But if you want that, why not just use Zig or Go?


Way to miss the point.

The program that is coded all in one go before ever compiling, and then never altered after, is rare as unicorns.

Any working program being altered will have almost all its borrows neatly lined up.


> The program that is coded all in one go before ever compiling, and then never altered after, is rare as unicorns.

What? I never said that. Its bit rich to say I've missed the point, then immediately misinterpret me.

> Any working program being altered will have almost all its borrows neatly lined up.

Sure. My question is: When the lifetimes don't make sense, what should the compiler do if its expected to produce running code all the same? Should it leak memory? Should it produce code with undefined behaviour - like use-after-free errors and race conditions?

You seem to want code thats been explicitly marked as invalid by the borrow checker to still run. What should it do when it runs?


Absolutely it should leak memory, if that is what it takes to get a program hobbling along. This is a debug build, we already know it doesn't work right, yet. If we didn't know before, the warning told us.

No program is going to ship with whatever bug it allows in, so who cares? The overwhelming majority of your undefined behaviors have no effect on what the person is interested in, right at that moment. They will get around to your crisis on their own time.

You are just insisting that your crisis be vanquished before they are allowed to look into theirs.


You then have two different languages. Its not a trivial decision to simply "leak" memory as you are saying. What happens with files? Do you corrupt the files? Do you not close TCP connections?

What you are proposing is to Rust to have two languages inside the same compiler. I guarantee you people would simply put debug builds in production if they could "turnoff" the borrow checker like you are proposing.


Do you X? Sure, why not?

I am not suggesting anything even vaguely like what you are saying.

The compiler should emit whatever the code it sees says to emit. If it neglects to arrange to close TCP connections, they won't be closed. So what? I am not interested in TCP connections right now, or I would patch up that bit, in response to the warning the compiler spit out. I have other things to worry about right then.

The release build will insist everything be taken care of, so there is no "other language". You are just permitted to get there by a route you choose, in place the route Cargo would impose now.


Having worked some in Rust, and a lot in Haskell in my experience the design changes because of the borrow checker/type system (fixing the issues will provoke re-design, mostly for the better). Meaning it's not something you just flip on after the fact. The types can help guide and drive the design. They aren't a straitjacket to be taken off once code whack a mole is done.

That said, I do think Rust could add something like Haskell's type holes where instead of just randomly leaking memory the code has undefined behavior and will simply crash the program if executed.


Nobody suggested "flipping it off after the fact", whatever that would mean. I have no idea where you are getting this.

> > Wanting to ship without violations does not mean you are not interested in getting your algorithm working, first. Often enough, you will abandon your laboriously borrow-safed code without shipping it, because you figured out a better way. But! you had to borrow-safe the new way, too, before you could even try it out.

This is a great idea! It made me realize why I still choose Python for prototyping. (I had been wondering...)

I absolutely love Rust's guarantees and prefer its memory management over GC. But nothing beats no memory management! Make everything Arc, make everything static---I don't care! My program will only live long enough to print "OK" and reach the next TODO, anyway.

> If your program confuses the borrow checker, that means that the compiler can't figure out when it should close that TCP connection on your behalf.

That's quite okay. "Process end" is as good a time as any, in this case.

> This isn't the sort of thing you can just turn off in debug mode, because programs should act the same in debug and release modes.

I don't know why programs with and without overflow and bounds checking are the "same" while programs that close unused sockets at different times are not, but it's okay---we don't have to call it "debug" mode. "Development" or "prototyping" mode both sound fine to me.

> There are ways around this - you can use types like Box, String, Rc, Cell, etc. And then just .clone() all over the place. But your code will run slowly. And thats still way more annoying to use compared to other languages. I don't know any other language which makes you write "Box" to put an object on the heap.

I think this is precisely the point. In other languages we can just create objects without worrying about where they should be stored. This is good because most of the objects -w-e- some people create don't get stored anywhere, anyway. They don't make it to the first release.

--

And yes, I realize I'm describing what is, from some angles, a completely different language. But I think one of the original discussion points is "why Rust-like languages keep popping up"? This is my take.

I didn't know where to squeeze this in, but the biggest annoyance in my (quite limited) time with Rust was: refactoring functions. I like breaking up functions into smaller chunks: `do_xy() { ... do_x(); ... do_y(); }` especially where a block of code could really benefit from being under its own name. Developing in Rust, I often found myself looking up internal types for the parameters/return type, and sometimes fighting the borrow checker. (I'm not sure type inference would be a good solution to the first, and I believe the second is mostly due to lexical lifetimes?)


To be pedantic, the borrow checker actually doesn't impact semantics.

The choice of which pointer type to use, e.g. a reference vs. Rc vs. Box, does impact semantics. And your broader point is well taken: since that choice affects semantics, it's really something you have to consider as you're designing your data structures; it doesn't really make sense to design the program first and tack on memory management at the end, like the parent is suggesting.

But that's not the borrow checker. For comparison, C++ has very similar behavior around smart pointers, but no borrow checker. Also, destructors are lexically scoped: a variable's destructor will be run when it goes out of scope, no earlier. The borrow checker used to be lexically scoped as well, but it's gotten smarter over time [1], and a borrow can now end in the middle of a block, if the borrow checker sees that the borrowed data is no longer used afterwards. Yet this has intentionally not been extended to include types with destructors, on the grounds that Rust code sometimes relies on destructor timing for correctness (in ways that the borrow checker can't see), so running destructors early could cause breakage, which would be hard for code authors to spot by just looking at the code. This same lack of predictability is considered acceptable for the borrow checker precisely because it doesn't affect runtime semantics.

[1] https://stackoverflow.com/questions/50251487/what-are-non-le...


Legal | privacy