Hacker News new | ask | show | jobs
by frodowtf 951 days ago
People say that OCaml is like Rust, but unlike Rust, OCaml has Exceptions that could appear everywhere. How is that safe?
6 comments

Rust has panics as well and they appear pretty much everywhere, because the Rust stdlb made a conscious decision to panic on allocation failures (later non-panicking APIs were added, but most people dont use them, and in special, most dependencies will not use this and will panic on random occasions), and also because common operations like integer division and array indexing will panic on bugs (and also integer overflow on debug builds)

In any case OCaml has memory safety, sort of (it has data races but as described in the paper "data races bounded in time and space", data races in OCaml doesn't lead to unrestricted UB like in Rust, C, C++, and most other languages actually) (unless you opt into OCaml's unsafe constructs like Obj.magic, which is like Rust's unsafe without using unsafe {} block), because it has a GC

So when you talk about safety in the context of exceptions, you probably mean exception safety rather than memory safety. Exception safety basically means you code works even if an exception is raised. Which is really hard to assure since as you said, exceptions are everywhere

But Rust suffer from this same problem! In Rust this is called panic safety and mitigating it has taken a great deal of complexity, with things like lock poisoning, which adds overhead but limit the scope of panics in multithreaded programs, and the UnwindSafe trait, which is probably a good attempt but is ignored in most of the ecosystem. Many people think such measures are inadequate and insufficient and prefer to run programs with panic=abort, which means to just terminate the program when there is any panic. (many C++ projects disable exceptions in the same way for example)

Which is kind of unfortunate because now there are many Rust programs that are only correct if you run with panic=abort, and will break if you enable stack unwinding (which is the default)

Rust currently cannot reliably panic on allocation failures because the error object is inside a Box, which itself requires allocation. This means that if memory is tight, panicking itself might fail.

For reference, this is the type that is returned from catch_unwind: https://doc.rust-lang.org/std/thread/type.Result.html

But I completely agree that Rust has exceptions. They are even used in the toolchain implementation for non-local control flow.

> Rust currently cannot reliably panic on allocation failures because the error object is inside a Box, which itself requires allocation. This means that if memory is tight, panicking itself might fail.

This seems like something that should be allocated at program startup, just like other things like the program's environment (I think it's copied to Rust's own data structures at startup to avoid using the non-threadsafe C API), and other things allocated at startup

.. but of course, not at Linux, such error allocation would be unneeded there..

.. except of you disable overcommit, which can and do happen, so in the general case you don't know if this error object can ever appear

The Box type, as used in the return type, transfers ownership to the calling function, so a check in the deallocator path would be needed to recognize this special object and avoid deallocating it. GCC has an emergency pool for its exception allocations, which is also quite ugly. And of course that pool can be too small.

It should be possible to add a third arm to that Result type, returning some &'static reference, but I'm not sure how to do it in a backwards-compatibile way.

I mean, allocate something like

static ALLOCATION_ERROR: Box<dyn Error> = (something);

and then use this variable whenever there is an allocation error. you might need unsafe { } but that's okay, the stdlib is full of unsafe

If you use it for producing the Box in the return value, you have to move out of it, and then it's gone. If you don't move it, you have a use-after-free bug after the first such panic has been caught and the box has been dropped as a result.
Safety was something added to Rust as it developed, not one of the original goals. As I recall it.

And you're working with multiple definitions of "safety" here, and Rust sorta conflates them all via borrow checker, but the one people are usually most concerned with is memory safety which is not a concern for a garbage collected language.

I do seem to recall that StandardML did not have exceptions though. And I always felt that SML was the better language.

OCaml adding OO classes and exceptions and other 90s trends that actually have ended up not aging well...

From the presentation introducing Rust to Mozilla: http://venge.net/graydon/talks/intro-talk-2.pdf

> I have been writing a compiled, concurrent, safe, systems programming language for the past four and a half years.

Safety was always part of it.

But those "Safety" definitions are all not like what Rust folks mean by safety now. He's talking about immutability and bounds checking and avoiding memory corruption but not at all about borrowing.

I guess I should have been more specific. If that's what we mean by safe, then OCaml is safe as well.

Anyways, I followed it at the time. The borrow checker came later.

I absolutely agree the borrow checker came later. I think of it as that the goals have always been the same, but the enforcement mechanism changed over time, as more and more static ways were found to achieve the goal.
I agree, Standard ML's syntax feels a lot cleaner than that of OCamls.
I think of OCaml like a "kitchen sink" language, much like Scala. The reference ML implementations like SML and Jersey seem to be much more limited in scope, and are nice for learning / getting a feel for the original intentions behind the language family.
Rust has panics that could appear anywhere.
But the flow control is easier to reason about. You don't have to go guessing about non-local catch blocks that the caller may have introduced. The code either panics, or propagates.

Exceptions look remarkably wrong headed to me in retrospect. Allowing the caller to change the error handling contract and flow control.

Rust panics can be catched too, but it's true that it's less common than in languages with exceptions
I don't like Exceptions in OCaml (or Haskell or C++ or JS/TS) either. But they aren't unsafe, except for bugs in the compiler/runtime (in OCaml, in C++ there are of course some footguns ;). Of course they add "bottom" to any function where an Exceptions can occur (and other things, see for example https://markkarpov.com/tutorial/Exceptions) but for stuff like `0/0` there are 3 possibilities:

use something like `Maybe` for the result - clumsy.

return (for example) 0, which is what most theorem provers (like Coq or Lean and dependently typed languages like Idris) do, that need their functions to be total

or throw an exception.

(C's solution of declari g it undefined behavior is missing).

Now, with OCaml's effect systém, there also is no need to use exceptions for control flow - which you should have never done anyway.

F# has exceptions. You can just pattern match on them.
In OCaml the type checker won't force you to handle exceptions (from what I remember.) See e.g. https://ocaml.org/docs/error-handling#exceptions
That's true, but at least in my experience, it is rarely a problem. Because if you're at a point in your program where you don't want to bubble up, you can just pattern match against the exceptions just as you would a Result type, which F# also has.

I don't know Rust, but after searching, it seems that it has a panic facility which seems even more escaping than an exception. Happy to be corrected there.

`panic` has nothing to do with error handling though. If you use it, you know that your callers cannot recover from it.

Throwing an exception implicitly delegates error handling back to the caller, but they are not even notified about it. (talking about OCaml)

No, they can, check the catch_unwind API.

Some frameworks catch panics automatically. For example, in the Actix web framework, if you panic in an HTTP response the panic will be catchee, so it won't bring the whole server down.

Also, by default a panic will terminate only the current thread, which is a major footgun: you now need to reason about what you program will do after some bug or unforseen circunstance happened somewhere in the code and made you program misbehave. Which leads to slightly insane things like lock poisoning.

It's more sane to compile with panic=abort, but that's not the default and it means that on panic you won't release resources (which aren't just allocated memory to be clear)

Yeah. I don't find it to be a problem either, but the parent does and I can see where they are coming from. Even Java has checked exceptions.
why do feel exceptions make a language unsafe?
Second, less-predictable execution path. Additional cognitive load in evaluating effects across both paths. Additional opportunity for bugs. Doesn't necessarily mean that exceptions make all software which uses them unsafe, but does tend to mean that exceptions significantly complicate the task of proving safety for any nontrivial program.
> proving safety for any nontrivial program.

I haven't done this, but probably the surest way to do that would be to use a proof assistant and extract the result to Standard ML or to OCaml. Standard ML has verified implementations, too.

Safe isn't the best word to describe it with. But it does mean that any expression or statement always has two possible control flows. You have the "surface flow" as well as the exceptional flow, so there's an added complexity.

I never felt this was a problem when I did Java though (despite their awkwardness - basically forcing coders to not use checked exceptions.)

Rust's control flow syntax for Results and Options are very similar to this but with an added benefit: you don't have to use the ?-operator.

panics is different, however. They are more akin to the way any Java program will happily OutOfMemoryError or NoClassDefFoundError given circumstances not (always) in your control.

I don't think panics are comparable to Java's VM errors. A lot of libraries (not just the standard library) seem to target panic-safety, avoiding unsafe behavior and resource leaks in case of panics. This means that the panic itself is not supposed to transition the process into an undefined state, like it happens with many VM errors in Java (where a stack overflow may mean that required cleanup action has not executed, for example).

With Rust, the overall situation is a bit strange: as a library author, you are expected to deal with the possibility of panics (which gives you all the headaches associated with dealing with exception safety), but as a user, you are not supposed to rely on them. (I expect that most request handler loops will have catch_unwind handlers, to avoid a faulty request taking down the entire process.)

I'm relatively new to Rust. I use panic (and cousins) in fn main only. As in: I'll expect(), unwrap() or similarly handle missing bootstrapping circumstances. Outside of main, I'll never ever use any of them. Even when I "know" that a condition is Impossible(tm).