
Rust RAII is better than the Haskell bracket pattern - setra
https://www.snoyman.com/blog/2018/10/raii-better-than-bracket-pattern
======
T-R
Something to keep in mind - linear types are on their way[1], with exactly
this usecase in mind. Simon Peyton Jones gave an excellent presentation on the
topic[2], briefly discussing exceptions, as well as giving a mention to
ResourceT and the phantom type solution in the article (described as channel-
passing).

[1] [https://arxiv.org/abs/1710.09756](https://arxiv.org/abs/1710.09756) [2]
[https://www.youtube.com/watch?v=t0mhvd3-60Y](https://www.youtube.com/watch?v=t0mhvd3-60Y)

~~~
thesz
Please, don't add them to the language. Use the library approach instead, it
is much more Haskellish.

~~~
radarsat1
Is it possible? I mean, to add linear types via a library? I feel like it
would have been done already if it were.

~~~
bjoli
I am always impressed by what the ocaml/Haskell people can do compared to my
language of choice (scheme).

Iirc Oleg Kiselyov implemented proper delimited continuations in ocaml as a
library, without touching the runtime or compiler. Something similar has been
done in Haskell.

I doubt fully dependent types can be implemented in Haskell without extra help
by ghc. There has been lots of work in the area, and last time I checked you
could simulate DT to some degree, but it never was as powerful as the
dependant types in idris. Iirc t were some edge cases where the typing became
undecidable.

~~~
fasquoika
>Iirc Oleg Kiselyov implemented proper delimited continuations in ocaml as a
library, without touching the runtime or compiler.

To clarify this, the library you're talking about implements most of the
functionality in C, reusing the runtime's exception mechanism. So it doesn't
require any upstream change to compiler or runtime, but it also can't be
implemented in pure OCaml.

~~~
bjoli
Hmmm. I remembered incorrectly then. The bytecode version is possible in pure
ocaml, but for native it apparently needs C.

For Haskell it is however possible. There is a neat paper by among others Kent
Dybvig.

------
kccqzy
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 have its merits; 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)).

Therefore the RAII style wouldn't really work in Haskell. The current bracket
approach is still better than RAII in Haskell.

That said, the ST-style trick of a phantom type variable is pretty well-known.
Unfortunately not many people knew the same trick can be used for non-ST as
well. I feel like as a community we should be encouraging this style more
often.

UPDATE: I wrote the original comment with the incorrect assumption that drop
functions will always be called in Rust. This is wrong. Please see child
comments.

~~~
jcelerier
> 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 this does not happen.

~~~
acqq
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.

~~~
kccqzy
Memory allocation and deallocation on the heap basically mean calling the
`operator new` and `operator delete` functions in C++. The language provides a
default implementation but you can override it.

Constructors are orthogonal. The job of a constructor is to construct your
object given that the space for the object is already allocated. This could be
on the stack, where allocation means bumping the stack pointer, or in-place in
preallocated storage (like std::vector), or the result of calling `operator
new`. Simply using the `new` syntax does both as a shorthand.

Similarly the job of a destructor is to destruct your object without
deallocating it. One can in-place destruct without deallocating, or destruct
and then deallocate implicitly when the stack pointer is adjusted, or not at
all. The `delete` syntax does both destruction and deallocation as a
convenience.

------
rbehrends
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.

The general problem — to make transactional state changes and transactional
control flow (i.e. expectations about these state changes) match up precisely
— is not easy to solve in the general case, especially once you move on to
things that are less trivial than simple resource acquisition/release
matching.

~~~
hawkice
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.

~~~
rbehrends
> 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<T> or Arc<T>, at which point you
don't really have lifetime guarantees anymore).

The essential limitation of Rust is that (without resorting to Rc<T> and
Arc<T>, 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.)

~~~
steveklabnik
> 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.

~~~
bunderbunder
> 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.

I'm not sure I follow.

The only reference-counted language I've used is (pre-ARC) Objective-C. There,
it was a very common idiom to "borrow" objects - so common that it didn't even
have a name. There was just objects you "retained" (that is, staked a claim
on), and ones you didn't.

Maybe there's a pitfall to how the "automatic" part of automatic reference
counting is typically implemented?

~~~
steveklabnik
It has been years since I've written objective-c, so I'll write out some
psuedo-code. This may be wrong, please correct me! (It should map to C++
pretty directly, and certainly does in unsafe Rust.)

* You have an object. You call retain on it. You have a count of one.

* You also have a pointer to that object. The "borrow" in your analogy.

* You return this pointer, and stash it somewhere. The object still has a count of one, so it's still live, so this is okay.

* Later in your program, you use that pointer to call release.

Here, we've only ever had a reference count of one, but our object has lived
across arbitrary inner scopes. In Rust, this would not work, unless you
dropped into unsafe.

Obviously, with Arc and autoretain this kind of code doesn't get written
anymore, I would hope. And even without, it wouldn't be guaranteed, so you'd
want the "borrow" to actually bump the refcount. But Rust is about
_guaranteeing_ that it can't.

~~~
bunderbunder
Ah, I think I follow.

So, it sounds to me like it's not necessarily that Rust's model is
fundamentally different from "ref counting with a limit of 1", at least in
terms of how you _should_ be managing your memory, so much as that the
language doesn't let you some things that you really shouldn't be doing in the
first place.

Sometimes it felt like Objective C wouldn't just let you point a gun at your
foot, it would actively cheer you on while you did it.

------
Animats
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.

~~~
simias
I don't think Rust can notify on a failing destructor other than panic!ing.
AFAIK the best you can do if you want to handle errors on close is to call
`flush()` (which does return errors) before dropping the object. Of course
that nullifies the benefits from RAII.

I don't know if there's an elegant way to solve this. If Rust had exception
you could use that but then again in C++ it's often explicitly discouraged to
throw in destructors because you could end up in a bad situation if you throw
an exception while propagating an other one. How does Python's "with" handle
that?

~~~
masklinn
> I don't think Rust can notify on a failing destructor other than panic!ing.

Much as in C++, this is not really allowed: drop runs during panic unwinding,
a panic during a panic will hard-abort the entire program.

> I don't know if there's an elegant way to solve this.

I don't really think there is. Maybe opt-in linear types could be added. That
would be at the cost of convenience (the compiler would require explicitly
closing every file and handling the result, you could not just forget about it
and expect it to be closed) but it would fix the issue and would slightly
reduce the holding lifetime of resources.

Furthermore, for convenience we could imagine a wrapper pointer converting a
linear structure into an affine one.

> How does Python's "with" handle that?

You'll get the exception from `__exit__` chaining to whatever exception
triggered it (if any). Exceptions are the normal error-handling mechanism of
Python so it's not out of place.

~~~
simias
>Much as in C++, this is not really allowed: drop runs during panic unwinding,
a panic during a panic will hard-abort the entire program.

Right, I didn't really consider that a "drawback" because I'm in the camp that
considers that panic! shouldn't unwind but actually abort the process here and
there anyway. But you're right that if you rely on the default unwinding
behavior panic!ing in destructors is a very bad idea.

~~~
cesarb
> But you're right that if you rely on the default unwinding behavior

You do rely on the default unwinding behavior anyway at least for `cargo
test`: the test framework depends on being able to catch the unwinds from
`assert_eq!` and similar.

------
mark_l_watson
Interesting! Michael is one of the more prolific writings and practitioners in
the Haskell space (I read just about everything he writes) so it is
interesting to also read his take on Rust.

------
it
Isn't this just because withMyResource returns IO a rather than IO ()? It
doesn't seem reasonable for it to return the resource.

------
EugeneOZ
Would be interesting to know why memory leaks are possible in Rust, if RAII is
so deeply integrated into language.

~~~
syn_rst
Like this:

    
    
      fn leak() {
          // Create a 1KiB heap-allocated vector
          let b = Box::new(vec![0u8; 1024]);
          // Turn it into a raw pointer
          let p = Box::into_raw(b);
          // Then leak the pointer
      }
    

Obviously that's kind of blatant, but there are more subtle ways to leak
memory. Memory leaks aren't considered unsafe, so even though they're
undesirable the compiler doesn't guarantee you won't have any.

Reference cycles when using Rc<T> are a big one, but generally it's pretty
hard to cause leaks by accident. I've only run into one instance of leaking
memory outside of unsafe code, and that was caused by a library issue.

~~~
the_mitsuhiko
The most obvious ways to leak are calling `Box::leak` which is also a very
useful API and `mem::forget` (the latter is mostly useful for working with
unsafe code).

------
hardwaresofton
tl;dr - Try rust.

The mechanic point of this article is pretty clear:

\- it's possible to be unsafe in both Haskell and Rust when dealing with
resource cleanup

\- Rust does a bit of a better job in the general case though it has it's own
warts (see the other comments, it's hard to deal with issues during
`drop`-triggered cleanup)

I want to make a muddier meta point -- Rust is the best systems language to
date (does anyone know a better one I can look at?).

\- The person who wrote this article Michael Snoyman[0] is mainly a haskell
developer, he's the lead developer behind arguably the most popular web
framework, yesod[1].

\- Haskell developers generally have a higher standard for type systems, and
spend a lot of time (whether they should or not) thinking about correctness
due to the pro-activity of the compiler.

\- These are the kind of people you _want_ trying to use/enjoy your language,
if only because they will create/disseminate patterns/insight that make
programming safer and easier for everyone down the line -- research languages
(Haskell is actually probably tied for the least "researchy" these days in the
ML camp) are the Mercedes Benz's of the programming world -- the safety
features trickle down from there.

\- Rust is _not_ a ML family language -- it's a systems language

\- People who write Haskell on a daily basis are finding their way to rust,
because it has a pretty great type system

When was the last time you saw a systems language with a type system so good
that people who are into type systems were working with it? When was the last
time you saw a systems language that scaled comfortably and gracefully from
embedded systems to web services? When have you last seen a systems language
with such a helpful, vibrant, excited community (TBH I don't think this _can_
last), backed by an organization with values Mozilla's?

You owe it to yourself to check it out. As far as I see it rust has two main
problems:

\- Learning curve for one of it's main features (ownership/borrowing)

\- Readability/Ergonomics (sigils, etc can make rust hard to read)

Admittedly, I never gave D[2] a proper shake, and I've heard it's good, but
the safety and the emphasis on zero-cost abstractions Rust offers me makes it
a non-starter. Rust is smart so I can be dumb. C++ had it's chance and it just
has too much cruft for not enough upside -- there's so much struggle required
to modernize, to make decisions that rust has had from the beginning (because
it's so new). It might be the more stable choice for a x hundred people big
corporate project today or next month, but I can't imagine a future where Rust
isn't the premier backend/systems language for performance critical (and even
those that are not critical) programs in the next ~5 years.

I'll go even one step further and say that I think that how much rust forces
you to think about ownership/borrowing and how memory is shared around your
application is _important_. Just as Haskell might force you to think about
types more closely/methodically (and you're often better for it), Rust's brand
of pain seems instructive.

[0]: [https://www.snoyman.com/](https://www.snoyman.com/)

[1]: [https://www.yesodweb.com/](https://www.yesodweb.com/)

[2]: [https://dlang.org/](https://dlang.org/)

~~~
sumofzeros
> When was the last time you saw a systems language that scaled comfortably
> and gracefully from embedded systems to web services?

Have a look at ATS[1], it supports many features that are available in Rust,
and let you build proofs about your code behaviour. It's quite type annotation
heavy though iirc, but it's very efficient.

[1] : [http://www.ats-lang.org](http://www.ats-lang.org)

------
platz
"RAII" is not "Rust"; "the bracket pattern" is not "Haskell"

~~~
steveklabnik
This is true, but the blog post uses these two as concrete examples, so the
title is still accurate.

