6 ms·
There's a difference. First, you're not incurring a large runtime penalty, so this works better in higher performance scenarios. Second, you're trapping and p
by steego 8y ago
There's a difference. First, you're not incurring a large runtime penalty, so this works better in higher performance scenarios.
Second, you're trapping and propagating successful values in a monad, which means your obligating the caller to handle both success and error conditions.
- jstimpfle 8y agoThe performance criticism is a common one, but I don't think most people actually know what they're talking about. I certainly don't, since I don't use exceptions, but I've heard many respected engineers say that they needn't be slow (it depends greatly on implementation). It shouldn't matter much anyway: You usually don't optimize for exceptional cases, especially if they lead to program termination. And furthermore, you should not make heavy use of exceptions anyway :-). They lead to non-local control flow that is extremely hard to follow. They are nice for short scripts where they print a stack trace and abort the program. But they are a huge maintenance burden in larger programs.
- steego 8y agoYou are correct that the performance of exceptions isn't typically needed because exceptions aren't meant to be thrown that often. They're exceptional. Railway oriented programming is better suited when errors are much more common in the ringtone and when their handling is a fundamental part of the rules you're encoding. One friend uses a Railway oriented approach precisely because throwing that many exceptions per second (I don't know the exact number) would severely undermine the performance of their system.
- jstimpfle 8y agoWhat is an example of exception that happens many times per second? And where it could not be easily avoided to step on the error path at all?
- steego 8y agoSo I'm not talking about exceptions, I'm talking about talking about errors. You can easily generate thousands of errors per second if you're simply validating and routing large streams of data.
- jstimpfle 8y agoIt's nice to get concrete and leave the abstract shit talk (of which I've been guilty many times). So let's focus on that problem you mention and not argue errors vs exceptions terminology. In the case you mention I would clearly make two streams of data. In the simplest case, two append-only dynamically allocated arrays, one for parsing errors and one for parsed results. This is very likely possible to do - unless one needs an exact ordering, in which case it could be still possible to add positions to each of the streams. It's intuitively clear to me that this approach will be significantly faster than a Result<>/Either based approach. Because control flow is much simpler. There are two consumers, one for the error stream, and one for the parsed stream. There is no switching between the cases on the consuming side, and complexity is much reduced because each part can do one thing and do it well. You also get the freedom to process the streams independently, e.g. at different points in time, or not at all, etc. It's much simpler - if you try this route once, you will ask yourself afterwards, how have you ever endured this complex algebraic data types stuff. Yes, you could still add a consumer of those Result<> values that demultiplexes them in two. But nobody does that. Because, why did we introduce Result values in the first place then? This is just one of the many examples I encounter daily and that demonstrate how advanced type systems and ideas like OOP or FP encourage bad program structure. They are just not good ideas.
- mdpopescu 8y ago> Yes, you could still add a consumer of those Result<> values that demultiplexes them in two. But nobody does that. What? That's exactly how you consume those values. In pseudocode: match result when success: ... when error: ...
- 8y ago
- junke 8y ago> 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?
- lmm 8y agoWith 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.
- jstimpfle 8y agoI 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).
- dnomad 8y agoThere is no performance penalty associated with try/catch. At least in JVM languages, try is free (its a compile time phenomenon) and throw is also free (memjmps), its only the creation of the exception that is expensive and this can be avoided by pre-allocating an exception (at the cost of no stacktrace info). Developers are better off in the end with proper exceptions because these sorts of Result objects don't scale. Haskell ultimately embraces proper exceptions and I suspect Rust will get there too eventually. F# actually has proper exceptions which makes you wonder why the author is reinventing the wheel badly.
- thardus 8y agoI've had a guy come in on an F# project that used exceptions in a relatively data intensive workflow and rewrite it to use Results like this. The amount of boxing and unboxing as you go through the various switches actually can ultimately make this much less efficient than the exception based flow. The code was ultimately something like 8 times as slow, and topped out CPU on beastly boxes which was surprising to me.
- bonesss 8y agoJust... impersible. You're describing a highly sub-optimal solution if there was performance degredation, and lots boxing/unboxing is a code smell. The biggest Exception penalty is only paid once when the exception code needs to be JITed along with more minor performance issues. Dollars for donuts, any CLR language will be faster using not-Exceptions, so a refactoring to model the exception path would have to do a fair bit work in unrelated areas to be slower.
- davidgrenier 8y agoI think you misunderstood his arguments. He's stating that favouring a Result-type monad (as opposed to using exceptions that may never occurs) leads to boxing of values inside a tag of a union-type. Threading exceptions handling where exceptions are never thrown in practice typically little cost. However this is really different from boxing of value-types in a situation where you don't have access to generic collections for instance and as such we're talking about a different cost. Unless you're entire application is passing integers or floats around (and not proper objects/records) it's highly unlikely the pattern is causing the situation he described.