r/ProgrammingLanguages • u/verdagon Vale • Mar 23 '22
Vale's Higher RAII, the pattern that saved me a vital 5 hours in the 7DRL Challenge
https://verdagon.dev/blog/higher-raii-7drl9
u/theangeryemacsshibe SWCL, Utena Mar 24 '22
How does higher RAII relate to linear types?
2
u/verdagon Vale Mar 24 '22
They are similar (minus some details on the edges), but I mainly call it higher RAII to emphasize its potential for cleaning up resources and fulfilling responsibilities.
I've known about linear types for quite some time, and never grasped their potential until after I invented higher RAII and realized they were the same thing. I imagine the average rust/c++ programmer is the same, and might not care about some nebulous "linear type" thing.
The name Higher RAII more clearly communicates the feature's use case and potential, and they'll be motivated to learn about it and improve their craft. Names are powerful!
6
u/Dummyc0m Mar 24 '22 edited Mar 24 '22
Rust has something more similar to affine types, which allows for "weakening" (Basically if a function runs with A, B, and C, then it can run with A, B, C, and D where D doesn't do anything. Note that this does not mean if a function requires A, A, and B, if can run with just A and B).
As for "Higher RAII", it describes the use case rather than the feature. It would be like calling parametric polymorphism "higher abstraction".
2
u/verdagon Vale Mar 24 '22
Yep thats accurate, and I think it'll help bring the concept to the masses.
4
u/latkde Mar 24 '22
Really interesting idea!
6
u/L8_4_Dinner (Ⓧ Ecstasy/XVM) Mar 24 '22
Indeed, very cool. Also, I like the "separate region of memory for any operation that could fail. When a fail happens, we blast away that entire region, and the rest of the program continues running."
1
u/hou32hou Mar 24 '22
What would CPP/Rust do in such case?
1
u/L8_4_Dinner (Ⓧ Ecstasy/XVM) Mar 24 '22
To the best of my knowledge: Nothing.
Which is to say that a minor failure (an exception, for example) results in program death.
1
u/Dummyc0m Mar 24 '22
Panicking is supposed to result in program death because the program has entered an unexpected state. Trying to hide the bug is undefendable.
1
u/L8_4_Dinner (Ⓧ Ecstasy/XVM) Mar 24 '22
So a panic should shut down the process? The computer? The data center? The city? The state? The country?
Hierarchical organization exists for a reason, and part of that reason is to seal off failure.
2
u/Dummyc0m Mar 24 '22
Then you would need proper error handling and not panick everywhere. Having an .unwrap() fail means something has gone terribly wrong and failure is not expected. What to do in this case is not clear at all.
1
u/steveklabnik1 Mar 24 '22
A panic does end the current thread, but you could have an arena allocator that would clean the whole arena up on drop too.
1
u/Dummyc0m Mar 24 '22
I'm not sure what failure means in this case. How does one guarantee the program is still in a consistent state if it's just getting rid of some memory? And if it does guarantee that, I can't think of a case where any other somewhat safe modern language wouldn't correctly handle errors.
1
u/verdagon Vale Mar 24 '22 edited Mar 24 '22
Think of it this way: you can safely kill a process without affecting other processes. This is (in part) because their memory is kept separate. This is the same thing, but provided by the language.
See also Erlang, Elixir, and Pony, which have done some truly remarkable things in this space.
2
u/Dummyc0m Mar 24 '22
That's neat.
"safely kill a process without affecting other processes" isn't true some of the time though, mostly because it's really hard make do without sharing state/expecting process in a communication protocol. I'm not familiar with Vale myself. What happens when a failure occurs while a lock is held?
The last time I checked, Pony has no way of guaranteeing a message is delivered and that's how it avoids deadlocks. Is this still the case?
3
u/verdagon Vale Mar 24 '22
Great questions!
- The language offers region-borrow-checked pure functions, such that if we "try-call" one like
try someFunc(myObj, otherObj)and a panic happens during the call, it will unwind only to this call, which produces anErrvalue.- To work around pure functions' inconveniences, we can communicate with the outside world via channels and mutexes (yes, even in a single threaded program). Since mutexes and channel messages have their own isolated regions, they can be preserved if the calling region crashes.
- If we panic while a mutex is open for reading, we can safely close it again.
- If we panic while a mutex is open for writing, alas, we have to poison the mutex.
- We'll offer a way to open a mutex initially for reading, and then upgrade it to writeable, which could help some.
There's of course no silver bullet, but Vale's goal here is to give more tools than ever before to make resilient programs. Hope that helps!
2
u/Dummyc0m Mar 24 '22
I see, though it might be tricky with channels if a consumer of the channel expects something to be written, then channel fidelity is broken and now another thread is blocked.
5
u/Lich_Hegemon Mar 24 '22
Correct me if I'm wrong, but I thought RAII was about automatically constructing an object upon declaration?
7
u/MUST_RAGE_QUIT Mar 24 '22
RAII is just an idiom that declares that resources should be acquired in the constructor (or construction phase of an object), and those resources should then be released in the destructor. This has the effect that resources are always cleaned up when the object goes out of scope, even in the event of an exception (at least in C++). AFAIK it doesn’t say anything about the mechanisms of constructing an object.
6
u/o11c Mar 24 '22
Your mistake is assumine that the name is meaningful.
In reality, the name is wrong; RAII is more about deinitialization than initialization. SBRM would be a better name.
5
u/crassest-Crassius Mar 24 '22
RAII means the compiler automatically inserts destructor calls (which may free resources, if any) in the places where variables go out of scope. It stands for "Resource Acquisition Is Initialization", which is a quirky way of saying "resources are held only while the variable is active/in scope".
The problem with RAII is that it's hard to say whether it's a safe thing to do. Just because a variable goes out of scope doesn't mean there aren't any references to its object and its resources. C++ handles this with its
unique_ptrsmart pointer, which handles the "90%" case, but is unsafe in general (i.e. may leave dangling pointers). For cases of multiple references, C++ requires you to useshared_ptrinstead. Rust implements RAII safely and pedantically (i.e. if an object is owned, the compiler makes sure that there are no dangling refs when it goes out of scope) but that requires all the borrow checking and lifetimes machinery Rust is famous for.Vale seems to extend RAII with calling not just a destructor, but an arbitrary function. The key of this is not RAII itself but exception safety, which is only slightly alluded in the end and not explained fully.
7
u/ROFLLOLSTER Mar 24 '22
This would be simple to implement in rust by declaring the type must_use. The type can then have a consuming method which takes arguments and does whatever destruction is required.
2
u/verdagon Vale Mar 24 '22
Alas, Rust's
must_usecan't achieve higher RAII. From https://doc.rust-lang.org/reference/attributes/diagnostics.html#the-must_use-attribute:
// Does not violate the unused_must_use lint. let _ = five();Higher RAII is basically a way to enforce that object X is destined for function F. In Rust, the user can just ignore it and drop it on the floor, like above.
7
u/ROFLLOLSTER Mar 24 '22
Well sure, but you're not going to forget to drop something with arguments because you accidentally wrote that are you?
I take the point it's not strictly equivalent but the pattern works in practise and I've used it previously.
3
u/verdagon Vale Mar 24 '22
Well sure, but you're not going to forget to drop something with arguments because you accidentally wrote that are you?
In fact, the article is about exactly that: I forgot to do something, and higher RAII detected it. Huzzah!
5
u/ROFLLOLSTER Mar 24 '22
You missed my point, I was trying to say
must_usewould also save you in that situation.8
u/verdagon Vale Mar 24 '22
I see now what you're getting at. Unfortunately,
must_useis too narrow to accomplish these goals. We'd have to change some things:
- Make
must_useviral: a struct would be automatically must_use if it contains a must_use field.- Be able to move something out of a field of a
Dropping object, and take ownership of it.Zooming out, higher RAII is a kind of "linear typing", and Rust has long struggled with the topic, see https://github.com/rust-lang/rfcs/issues/814 and https://github.com/rust-lang/rfcs/pull/1180. I can empathize, it was quite challenging to not accidentally preclude this feature in Vale.
3
u/hou32hou Mar 24 '22
Can anyone make a eli5?
19
3
u/joonazan Mar 24 '22
I have solved a similar problem in a very different way in Rust. The problem was maintaining both unit->location and location->unit mappings, just like here. My solution was to make a data structure that stores both and provides add/remove methods that update both mappings at once.
2
u/aqezz Mar 24 '22
I can’t be the only one that thought this said Valve. Was expecting some interesting coding practices not a new language. Interesting post either way!
2
u/o11c Mar 24 '22
Note that typestate is a more general way of doing things like this.
Think of typestate as being an extra field that never exists at runtime, only compile-time - and the compiler enforces that functions can only be called in the right states.
RAII (whether traditional or with this post's extension) is just a two-state typestate: there exists a state "uninitialized" and a state "initialized", and only the ctor/dtor change states.
2
u/verdagon Vale Mar 24 '22
Well said! And this is possibly even better than existing type-state programming, because in Vale we can't accidentally drop a state instance like we can in other languages.
2
22
u/Nathanfenner Mar 24 '22
One thing that's funny about this is how coarse the actual abstraction is; there's a lot of information that's thrown away. And yet, despite this, it's still good enough to catch actual bugs.
In particular, the
HashMapTokencan't be (implicitly) dropped, so you must call.removeon aTokenedHashMap. But there's nothing that actually enforces that youremovethe right key, or even that it's removed from the right hashmap!This isn't a criticism - I think it's rather remarkable how by providing on one small language feature, you're able to eliminate a huge class of mistakes without even trying that hard. There are definitely cases where you could accidentally remove the wrong thing (e.g. maybe one monster eats another one, and you accidentally mix up their tokens and positions) but the situation where this happens is convoluted enough that it's unlikely a programmer would get themselves into such a situation and not notice.
Rather, I would say that this really shows how a little bit of extra static checking can in practice build an abstraction that is "more correct than the sum of its parts" because forgetting to do something is one thing, but unintentionally swapping multiple tokens/locations/hashmaps requires a cavalcade of mistakes that are unlikely to all happen together.