> First, you're not incurring a large runtime penalty
How large, and why? Only when exceptions are thrown, or also during the happy path?
From what I understand, with Monads the code check errors after each expression (like in C/Go), but behind the scene. Isn't it the case?
If so, wouldn't that incur an overhead that is not present with exceptions, where normal code runs without checks and where "throws" perform a sophisticated jump?
With exceptions you have overhead on the happy path and potentially even in code that can never error: you have to keep all your old stack frames around in case you throw, since you never know when you might throw. Doing nice tail calls, or continuations, gets very hard since you have to figure out how your exception handling interacts with them. (Of course F# has to pay a lot of those costs already, since it runs on a VM that was designed for a language with exceptions). Whereas with result types everything is just functions and values and you don't need any special cases.
I consider myself a skilled programmer that is aware enough of low level issues to usually write faster alternatives to any "not quite optimal" code on the first try, without resorting to any micro-optimization. That includes rewriting code that makes use of libraries like STL of which most people will just say "dude, you are insane, you can't be faster than the STL. They put so many man-years of effort into that".
But it rarely matters how much work you put into something to make it faster. What matters is what you can avoid to do at all.
I have pretty much zero knowledge of CPU-level issues and barely know enough x86 assembler to read my compiler's output. I have never cared for optimizations like inlining code, vectorization or tail call elimination. I have tried inlining a few times, and the speed-ups it brought were negligible. (And the slow-downs that too much of inlining brings, by increasing code size and thereby increasing cache pressure, must be very subtle and basically unmeasurable, while being potentially enormous. And the disadvantages from a maintainability standpoint are considerable as well).
If you have to care about these things, then maybe you're doing too much work. (Well, I guess I would use vectorization/SIMD if I had to write a video codec, but I've never done that).
Avoiding Exceptions in .NET isn't some micro-optimization that makes a tiny difference in runtime speed...
Exceptions are pricey pricey things that take time and create some minorly interesting VM/GC behaviour that can impact your application. If at all possible they should be avoided in executed code.
> But it rarely matters how much work you put into something to make it faster. What matters is what you can avoid to do at all.
Using Result<> types to handle error cases is a very good example of doing just that: avoid the big hit of exceptions and poor exception handling in cacadingg situations with sensible return types that let you avoid a buttload of delicate work.
> Exceptions are pricey pricey things that take time and create some minorly interesting VM/GC behaviour that can impact your application.
This is all very vague and handwavey. Not sure about the .NET world but in other systems the costs of exceptions is not significant and they can be used successfully even in the most demanding low-latency applications.
> Using Result<> types to handle error cases is a very good example of doing just tha
Result types don't scale. I suppose it's the sort of thing that programmers have to keep rediscovering for themselves but there's a reason why even Haskell eventually "discovered" exceptions (and I suspect the Rust guys will get there eventually). In the real world where you have lots of deeply nested function calls some of which cross component boundaries good error handling requires three things: contracts (you need to be able to express possible errors at boundaries), context (you need to know exactly what resources have been allocated so they can be cleaned up when an error is caught) and recovery (you need to be able to return the application to well-defined state/point so computation can continue). Frankly, Result types don't give you any of these. What you end up with is a very generic set of errors (necessary to avoid a combinatorial explosion of error types) and a poor-man's implementation of exceptions (see panic/recover in golang). Or I suppose you can write code that doesn't involve deeply nested call-graphs and multiple components and everything is perfectly "flat" and stateless... but this wouldn't be like any real application I've ever seen.
Even so, exceptions are generally not used for error handling in Haskell, yet life seems to go on. You'll see code using `throwError` or `catchError`, but usually these will just be providing a nice bit of sugar on top of an Either or Maybe type. So it seems like the takeaway should be that Result types do scale, with proper library+language support.
We sorta use exceptions for IO-based code, mostly because we love parallel combinators in async and they totally require async exceptions to work right.
What I was trying to say somewhere between the lines: Try structuring your code so you don't run into errors in the first place. Because if error handling performance matters, you're having too many of them.
This is always good advice. One should strive so their code doesn't run into errors in the first place.
However, sometimes the errors are actually part of the fabric of the domain itself. They're expected to occur very frequently. The data it encodes is valuable. The way they're handled is important.
Nevertheless, you're right. More often than not, you're better of using traditional exception handling and preventing errors from occurring in the first place.
Oh, I agree. I'd use results rather than exceptions even if they were slower (and in some implementations they are), because they make the code so much easier to understand and maintain and that's more important the overwhelming majority of the time. But for what it's worth, correctly implemented they can also be faster. And I find that's generally the case: a clear, simple implementation that expresses the essence of the problem will be faster than any number of carefully-tuned edge cases.
> Only when exceptions are thrown, or also during the happy path?
This is where I want to make a key distinction between exceptions and errors ala Railway programming. Performance with exceptions doesn't really matter because exceptions are supposed to be exceptional.
With Railway oriented programmer, the whole point is that you're encoding errors into the logic of some type of system. These types of errors may not only be not exceptional, they may a very regular part of the application.
Like I told another commenter, a friend of mine uses them in an application where they easily would throw thousands of exceptions per second if they used exceptions. Not only would it undermine the performance of their system, it's using the wrong tool for the job.
How large, and why? Only when exceptions are thrown, or also during the happy path?
From what I understand, with Monads the code check errors after each expression (like in C/Go), but behind the scene. Isn't it the case? If so, wouldn't that incur an overhead that is not present with exceptions, where normal code runs without checks and where "throws" perform a sophisticated jump?