However, there are ways in which my own function can fail. So I suppose I need my own wrapper error:
struct Error<E> {
// one could perhaps argue this case is a contract
// violation and panic-worthy, but I'm just trying to
// include more than one error in the example
BadBounds,
NoMinimum,
ComputeError(E),
}
fn bisect<E>(
(lo, hi): (f64, f64),
compute: &mut FnMut(f64) -> Result<f64, E>,
) -> Result<f64, Error<E>>
But it doesn't appear to me that, for instance, error chain supports generic errors. So then I have to write a bunch of boilerplate and decide on arbitrary-feeling bounds like Error + Send + 'static or whatnot. Also, surely the calling code cares a lot more about its own errors than those produced by this library, so erasing the type of E feels like a non-option. Even putting my own errors in that same channel feels invasive.
How do others handle this sort of situation in a manner that places reasonable expectations on the user?
I've done this a couple of times and felt bothered about deciding which way to order the errors.
Coming from the Haskell mindset of monads, I have a natural tendency to think E belongs on the outer Result. We take a function that short-circuits to E and produce a function that does the same. In haskell, it's what you would end up if you started out with a bisect function that took an a -> b callback and then created a monadic bisectM variant whose callback is a -> m b.
But then if I had to consider which case deserves the label of a "successful failure", it would be E, not my error.
That said, it does compose the most easily and with the least boilerplate.
I think of it as follows: user function returns Result<f64, E> - without your function's involvement that's what a caller handles. But, your call occurs first and mostly does error checking - as such, it's only real return type of its own is YourError. The Result<Result<...>, YourError> seems to convey that nicely.
To be clear, the user's function in this case gets called multiple times, and bisect short-circuits as soon as it encounters an E. NoMinimum is an error case that can only be determined after many successful calls to the user function.
That said, I do suspect that my reasoning for putting E on the outside mostly stems from me trying to bring in the paradigms of another language.
Right, makes sense. I still think the separation of the results is best - your code is the driver of the caller's code, doesn't provide the "true" Ok result itself (ie the f64 here), and they fail in completely different manners; your failures are more to do with failing to drive the caller to successful completion, whereas their failures are something else entirely (opaque).
But I can see an argument for blending them too. I'd go with the separation myself, I think.