if (array.len() >= 2) {
let pivot = array[array.len() / 2];
...
}
Those brackets could panic, if the index was out of bounds! Of course you can tell locally from the code that it can't be out of bounds. But if you're really not allowed to have panics (and equivalently array accesses with square brackets, as those can panic), then you have to write this code instead: if (array.len() >= 2) {
let pivot = match array.get(array.len() / 2) {
Some(pivot) => pivot,
None => return Err(InternalQuicksortError("bad index somehow")),
};
...
}
You do this all over your codebase and it gets hard to read very quickly!EDIT: clarified code
If there's any question as to whether a line might actually panic or not, then absolutely, generate helpful error messages. But when you can tell through simple local reasoning that the panic will never happen, you can just use square brackets. It's OK.
A problem is that you may be able to tell now, but as code changes and functions grow and split, that reasoning may grow less simple and less local without any changes of those lines in particular.
The odds of that, and the degree to which it matters, are of course both spectacularly situation dependent.
The point being, take context into account. You can't code like everything everywhere is going to get modified to be worse.
#[derive(thiserror::Error, Debug)]
enum Error {
#[error("bad index somehow")]
BadIndex
}
// ...
let pivot = array.get(array.len() / 2).ok_or(Error::BadIndex)?;I claim this is far worse than panic in this case... you've now introduced a code path variant that folks above think is "legitimate" or caused by some input or something in the environment that could be changed to make it go away... but that's not the case here... the only thing that can be fixed is the code in question to not be in this region if the array length is 2 or less. It's a plain old "bug" and the only cure (other than more cowbell) is to modify source and recompile.
Applications do many things. A degraded state (e.g. a single endpoint being broken) is often preferable to a full crash. It's very little effort to opt in to panics by calling unwrap(). It's a lot of effort to recover from panics. Let me, as the caller, make that decision.
Some technical limits, a lot of cultural opposition. While it is possible to have a panic handler, it's generally viewed as a bad idea. Not everything is UnwindSafe, there are limits to how much a panic handler can do, concerns about memory leaks.
Again, the caller can turn it into a panic easily enough if that's what they want. leave the decision in their hands.
let lo, hi = array.bounds()
match lo < hi {
None => array,
Some(lo, hi) => {
let pivot_index = lo.avg_with(hi) // guaranteed to be in bounds
let pivot = array[pivot_index] // never panics, but the index type is opaque, not usize
...rest of the qsort here
}
}https://plv.mpi-sws.org/rustbelt/ghostcell/paper.pdf https://gitlab.mpi-sws.org/FP/ghostcell/-/blob/master/ghostc...
I personally think that taking an optimization (e.g. boundary checks elimination) and bringing it — or more precisely, the logic that verifies that this optimization is safe — to the source language-level is a very promising direction of research.
I do not believe you've rebutted the main point here. At best what you've done is a show a better way of guaranteeing that indices are correct. But you still need to deal with the fact that index notation comes with a panicking branch.
Not necessarily? Indexing is overloadable, so nothing stops one to write an implementation that'd use an unsafe block without any checks/panics whatsoever (because there are static guarantees that those would be unnecessary).
I think this is continuing to miss the forest for the trees. The original code snippet could also just use `unsafe` to elide the panicking branch and it would still be just as correct while satisfying the requirement that there are no panicking branches.
But what's actually happened is that you've traded "panic when a bug occurs" with "undefined behavior when a bug occurs." Both things are valid choices depending on the circumstances, but the latter is not a realistic escape hatch out of the discussion at hand: whether one should completely elide all panicking branches.
The comment that kicked off this sub-thread:
> IMO (systems programming background), the litmus test for panic! vs. Result doesn’t exist. Don’t panic.
The "Don't panic" advice is correct but ambiguous on its own. If it's referring to the behavior of a program, then yes, absolutely, don't panic. Or as I like say, "if a program panics, it should be considered a bug." But if it's referring the source code, as in, there should be no panicking branches, then that is absolutely wrong. Neither the standard library nor any popular Rust library follows that practice. And they shouldn't.
I interpreted "Don't panic" as the latter meaning because of the claim that "panic! vs Result doesn't exist." That only makes sense in the latter interpretation. In the former interpretation, one does need to contend with a "panic vs Result" choice, as it's often a balance between convenience and how it's usually used. For example, indexing a slice with a `usize` panics.
So either the former interpretation was meant and the poster is confused (or just misspoke), or the latter interpretation was meant and is divorced from reality.
I'll post a link to my blog on this topic yet again, because I'm pretty sure it will clarify the situation and my position: https://blog.burntsushi.net/unwrap
I will concede that the “you should never get here” type errors are tempting to panic on. But I have seen so many instances where the “you should never get here” code does execute. Memory corruption, access by other processors, etc., could make this check (which should never happen!) actually fail at runtime. If one of those happens in code I wrote, I would rather be alerted to the error, without crashing, if possible.
A lot of the appeal to “panic on error” IMO is to reduce developer cognitive load. As a developer, it’s inconvenient to consider all the errors that can actually happen when programming. Allocations, array accesses, I/O, and other errors programmers want to ignore, do happen. It’s annoying to think about them every time we write code, but I’d rather have something that forces me to consider them, rather than be bit by something that I designed out of my consideration.
This preference might change depending on what I’m writing. For a one-off script that I need to spin up quickly, I don’t really care. For code running on a device I may not get to service for 5 years, my opinion would be different.
Rust has the ? operator to address the common pattern of returning a failure condition to the caller. There's effectively zero cognitive load involved, since compiler hints take care of it when writing the code.
> I’d be curious to see if the optimizer couldn’t detect in this instance that that code is, in fact, unreachable.
Probably, but that should be a job for the type checker not the optimizer. Ideally, there should be a way of creating statically verified asserts about program values, and plugging them in as preconditions to function calls that require them for correctness.
FYI Rust panics and illegal `[]` accesses will tell you the file & line number where it happened.
Cognitive load is one of the main blocks to development. Do not diminish the importance of lowering cognitive load
> This preference might change depending on what I’m writing. For a one-off script that I need to spin up quickly, I don’t really care.
Yes, absolutely. Trade offs
Rust does have Results that it knows can't fail - for genericity reasons - these are Infallible results, for example try_into() when into() would have worked is Infallible, Rust uses an empty type (equivalent to but for now not literally identical to ! aka Never) for this, so the compiler knows this type can't be reified, it needn't do code generation for this never-happens error.
But there's a crucial distinction between Rust's compiler can see this is Infallible and I can show it won't fail and Rice's theorem means that gap is insurmountable even just in principle.
This is basically how you are using this Option by the way, and while it's possible, you have to consider why the original author decided to return an Option and not a Result: It's because he wants you to handle that in a way that doesn't fail (ie: an error) but instead consider it as a non-failure possibility.
When rust panics, it shuts the program down.
When linux panics, it halts the machine - for fundamental things like data corruption detected, and we can't continue to operate and risk corrupting the disk. Just like Windows has the BSOD.
But also the rest of the blog post should help clear up the confusion.
if !cond {
// I ad-libbed the exact error message
panic!("Asssertion failed {cond} was false, line/file information, etc")
}
panic!(message) is "either throw an exception containing message, or print the message + maybe a backtrace and abort the process".You can use compiler flags to guarantee that it aborts instead of throwing an exception, you can't use compiler flags to guarantee that it acts an exception, sometimes rust will just abort the process (for example if you panic!() while you're already unwinding because of a prior panic!()).
If nothing catches the exception (and that's usually the case, by convention), the runtime will print the message, maybe a backtrace, and kill the thread that paniced.