Live data from Hacker News

Rust RAII is better than the Haskell bracket pattern

snoyman.com

61–70 of 156 posts

Re: Rust RAII is better than the Haskell bracket pattern

#61
post #53

You can also have the exact opposite problem with RAII, where a resource survives the end of a transaction, because there is still a live reference to it hidden away somewhere (say, due to some debugging code holding on to it). This is a classical liveness vs. safety dualism. "Something good will eventually happen" and "nothing bad will ever happen" are promises whose solutions are often in conflict with one another.…

Oddly, Rust's ownership system really does solve these problems, and Non-lexical lifetimes should eliminate accidental scope-broadening. Unless you are doing some mega-schenanigans, an e.g. MutexGuard gets released precisely when you think. Your point about this being difficult to solve in the general case is true, it's just worth pointing out Rust intends to do that hard thing anyway.

> Oddly, Rust's ownership system really does solve these problems

No. Rust's ownership problem solves it for trivial cases, at the cost of making it hard to do other things (such as sharing references past the lifetime of the owner without resorting to Rc or Arc, at which point you don't really have lifetime guarantees anymore).

The essential limitation of Rust is that (without resorting to Rc and Arc, which would put you back to square one) it is conceptually limited to the equivalent of reference counting with a maximum reference count of 1. In order to make this work, Rust needs move semantics and the ability to prove that an alias has a lifetime that is a subset of the lifetime of the original object) and may even sometimes have to copy objects, because it can never actually increase the (purely fictitious) reference count after object creation.

This inherent limitation makes a lot of things hard (or at least hard to do without copying or explicit reference counting). Structural sharing in general, hash consing, persistent data structures, global and shared caches, cyclic data structures, and so forth.

In short, you have the problem with shared references less, because Rust makes it hard to share data in the first place, for better or worse. (Again, unless you resort to reference counting, and then you get the issue back in full force.)

Re: Rust RAII is better than the Haskell bracket pattern

#62
post #50

Earlier quoted context omitted.

> when many C++ and Rust programs are about to end, they spend the last few cycles uselessly deallocating memory that would've immediately been freed via _exit(2) thank god they do this. how many times did I have to manually force linux to release sockets because badly coded C programs which opened sockets forgot to release them causing them to hang up for ~5 minutes after the process ended. With proper RAII classes…

Your example combined with the parents observation show that C++ put under the same construct the concepts that should be separated: memory allocation should be handled differently from the construction, destruction and other resource allocation.

Memory allocation/deallocation in C++ is handled separately from construction and destruction. The delete and new syntax is short hand for combining the two.

Re: Rust RAII is better than the Haskell bracket pattern

#63
post #53

Earlier quoted context omitted.

Oddly, Rust's ownership system really does solve these problems, and Non-lexical lifetimes should eliminate accidental scope-broadening. Unless you are doing some mega-schenanigans, an e.g. MutexGuard gets released precisely when you think. Your point about this being difficult to solve in the general case is true, it's just worth pointing out Rust intends to do that hard thing anyway.

> Oddly, Rust's ownership system really does solve these problems No. Rust's ownership problem solves it for trivial cases, at the cost of making it hard to do other things (such as sharing references past the lifetime of the owner without resorting to Rc or Arc , at which point you don't really have lifetime guarantees anymore). The essential limitation of Rust is that (without resorting to Rc and Arc , which would…

> it is conceptually limited to the equivalent of reference counting with a maximum reference count of 1.

This is a thing people say, but I think it's misleading. Reference counting can increase the lifetime of an object, but borrowing cannot. I've seen this really trip up beginners.

> This inherent limitation makes a lot of things hard

It can make them different, which can be hard, but these things are already hard. And some people think it can make them easier or even better; see Bodil Stokke's work on persistent data structures in Rust.

Re: Rust RAII is better than the Haskell bracket pattern

#64
post #45
post #10

How do you handle errors at resource release? When you close a file, the final writes take place, and they can fail. What's the idiom in Rust for getting them out? Python's "with" clause, and the way it interacts with exceptions, is the only system I've seen that gets this right for the nested case.

I do prefer the "with"/"try-with-resources" approach because it is explicit. With RAII in C++ there's no visual difference between dumb data objects and objects like locks that are created and held on to mainly to cause implicit side effects. In Rust this also prevents the compiler from dropping objects early - everything must be held until the end of its scope for the 0.1% of cases where you're RAII managing some ex…

You can call std::mem::drop if you'd like to drop something early. That's the notation you're asking for.

Additionally, part of Rust's core ideas is that the compiler has your back with this kind of thing, so there's less need for comments that say "CAUTION HERE BE DRAGONS." Those things can still be useful for understanding details of your code, of course, but they aren't needed to ensure that things are memory safe. That's what the compiler is for!

Re: Rust RAII is better than the Haskell bracket pattern

#65
post #50

Earlier quoted context omitted.

> when many C++ and Rust programs are about to end, they spend the last few cycles uselessly deallocating memory that would've immediately been freed via _exit(2) thank god they do this. how many times did I have to manually force linux to release sockets because badly coded C programs which opened sockets forgot to release them causing them to hang up for ~5 minutes after the process ended. With proper RAII classes…

Your example combined with the parents observation show that C++ put under the same construct the concepts that should be separated: memory allocation should be handled differently from the construction, destruction and other resource allocation.

> memory allocation should be handled differently from the construction, destruction and other resource allocation.

These other resources still need an in-memory representation to track and reference resources, so you can't really separate them.

Re: Rust RAII is better than the Haskell bracket pattern

#66
post #18

Earlier quoted context omitted.

I don't know if it's fair to call that "the Haskell approach", per-se. That destructors are not guaranteed to run, or run predictably, is generally a property of all fast garbage collectors. If you want a GC that can run quickly, which in a language like haskell where you're going to get lots of small allocations in contexts it would be difficult-to-impossible to efficiently determine the exact moment scopes die, or…

You can have both, GC, static allocation and RAII, like in Modula-3 for example. Which .NET is finally arriving to, thanks to Midori outcomes. And Java might eventually get there as well, depending on how Project Valhalla ends up. As for languages like Haskell, a mix of bracket and linear types might be the way to go.

I googled "Midori outcomes", but I only found your posts mentioning it. Have a better keyword to search or a link?

Re: Rust RAII is better than the Haskell bracket pattern

#67
post #2

The thing is that, in Haskell, even when you attach a function to run during destruction, the runtime doesn't guarantee that the function will be called promptly, or even at all. Rust drops (runs destructor and deallocates) values as soon as they go out of scope; C++ too. In Haskell you depend on the whims of the GC, which makes RAII unusable. (The Haskell approach of not guaranteeing destructors being called does ha…

> when many C++ and Rust programs are about to end, they spend the last few cycles uselessly deallocating memory that would've immediately been freed via _exit(2)

This isn't useless because memory allocation can happen during destruction/exit, e.g. to write some data to the filesystem.

Suppose you have a container with a billion objects. The container's destructor iterates over each object, doing some housekeeping that requires making a copy and then deleting the original before moving on to the next object.

That requires memory equivalent to one additional object because an original is destroyed following each copy. Stop dellocating memory during destruction/exit and the total memory required doubles, because you have all the copies but still all the originals.

There are also some helpful things that happen during deallocation. For example, glibc has double free detection, which strongly implies potential UAF but it's only detected if the second free() actually gets called.

Re: Rust RAII is better than the Haskell bracket pattern

#68
post #45

Earlier quoted context omitted.

I do prefer the "with"/"try-with-resources" approach because it is explicit. With RAII in C++ there's no visual difference between dumb data objects and objects like locks that are created and held on to mainly to cause implicit side effects. In Rust this also prevents the compiler from dropping objects early - everything must be held until the end of its scope for the 0.1% of cases where you're RAII managing some ex…

You can call std::mem::drop if you'd like to drop something early. That's the notation you're asking for. Additionally, part of Rust's core ideas is that the compiler has your back with this kind of thing, so there's less need for comments that say "CAUTION HERE BE DRAGONS." Those things can still be useful for understanding details of your code, of course, but they aren't needed to ensure that things are memory safe…

I do use explicit drop() calls in my own code to call attention to drops with side effects, but it does not seem to be common practice.

My preferred semantics would have been early drops by default, and a must_drop annotation similar to must_use, to say that objects like RwLockReadGuard should be explicitly dropped or moved.

Re: Rust RAII is better than the Haskell bracket pattern

#69
post #68

Earlier quoted context omitted.

You can call std::mem::drop if you'd like to drop something early. That's the notation you're asking for. Additionally, part of Rust's core ideas is that the compiler has your back with this kind of thing, so there's less need for comments that say "CAUTION HERE BE DRAGONS." Those things can still be useful for understanding details of your code, of course, but they aren't needed to ensure that things are memory safe…

I do use explicit drop() calls in my own code to call attention to drops with side effects, but it does not seem to be common practice. My preferred semantics would have been early drops by default, and a must_drop annotation similar to must_use, to say that objects like RwLockReadGuard should be explicitly dropped or moved.

Those semantics would be nice, but they have a lot of unsolved issues: https://gankro.github.io/blah/linear-rust/

Re: Rust RAII is better than the Haskell bracket pattern

#70
post #10

How do you handle errors at resource release? When you close a file, the final writes take place, and they can fail. What's the idiom in Rust for getting them out? Python's "with" clause, and the way it interacts with exceptions, is the only system I've seen that gets this right for the nested case.

Java has the try-with-resources statement, and C# has the using statement. They're alternative forms of the try statement and they're functionally equivalent to Python's with statement using contextlib.closing.
Post reply on HN