▲ | wesselbindt 2 days ago | |||||||||||||||||||||||||||||||
I do not work in a functional language, but these ideas have helped me a lot anyway. The only idea here that I find less directly applicable outside purely functional languages is the "Errors as values [instead of exceptions]" one. On the surface, it makes complete sense, the non-locality of exceptions make them hard to reason about for the same reasons that GOTO is hard to reason about, and representing failure modes by values completely eliminates this non-locality. And in purely functional languages, that's the end of the story. But in imperative languages, we can do something like this:
and a client of this function might do something like this:
and completely ignore the failure case. Now, ignoring the failure is possible with both the exception and the failure value, but in the case of the failure value, it's much more likely to go unnoticed. The exception version is much more in line with the "let it crash" philosophy of Erlang and Elixir, and I'm not sure if the benefits of locality outweigh those of the "let it crash" philosophy.Have any imperative folks here successfully used the "errors as values" idea? | ||||||||||||||||||||||||||||||||
▲ | skirmish 2 days ago | parent | next [-] | |||||||||||||||||||||||||||||||
Rust does "errors as values" pretty well, see [1]. You can manually handle them if you want, or just apply the '?' operator to auto-propagate them out of the function. In both cases, it is obvious that there was an error handling needed. [1] https://doc.rust-lang.org/book/ch09-02-recoverable-errors-wi... | ||||||||||||||||||||||||||||||||
| ||||||||||||||||||||||||||||||||
▲ | TeMPOraL 2 days ago | parent | prev | next [-] | |||||||||||||||||||||||||||||||
Non-locality of exceptions is a feature, not a bug. It's so you can focus on the success case, instead of error case, when reading your code. It's usually, but not always, what you want. "errors as values" is effectively the same thing as exceptions anyway, except it's explicit - meaning it's hurting readability by default and adding extra work[0]; modern languages go to extreme length to try and paper it over with syntactic magic. Unfortunately, the whole "exceptions vs. expected" issue is fundamentally unsolvable for as long as we stick to working on common single source of truth plaintext code. Explicit and implicit error handling are both useful in different circumstances - it's entirely a presentation issue (i.e. how code looks to you); our current paradigm forces us to precommit to one or the other, which just plain sucks. -- [0] - See how it works in C++, where you can't easily hide the mechanism. | ||||||||||||||||||||||||||||||||
| ||||||||||||||||||||||||||||||||
▲ | nextaccountic 2 days ago | parent | prev | next [-] | |||||||||||||||||||||||||||||||
The way Rust solves this is that the type Result is marked with #[must_use] - ignoring it and not deciding what to do with the error raises an warning. Plus, if you want to bubble the error, you must intentionally use the ? operator, so you are forced to acknowledge the function you called actually may raise an error (as opposed to calling an API you're unfamiliar with and forgetting to check whether it can raise an error, and the compiler not having your back) | ||||||||||||||||||||||||||||||||
▲ | galaxyLogic 2 days ago | parent | prev | next [-] | |||||||||||||||||||||||||||||||
The way to do functional programming in imperative languages is to handle the side-effects as high up in the call-chain as possible. That would mean that you return an instance of Error from lower-level and decide in some higher caller what to do about it. That as an alternative to throwing the error. This way you get the benefit of being able to follow the flow of control from each called function back to each caller, as opposed to control jumping around wildly because of thrown errors. In a statically typed imperative language that would need support for sum-types, to be able to return either an error or a non-error-value. Then you would be less likely to ignore the errors by accident because you would always see the return value is maybe an error. Isn't this also basically how Haskell does it, handling side-effectful values as high up as as possible which in Haskell means moving them into the runtime system above all user-code? | ||||||||||||||||||||||||||||||||
| ||||||||||||||||||||||||||||||||
▲ | klysm 2 days ago | parent | prev | next [-] | |||||||||||||||||||||||||||||||
C# nullable reference types with our parameters can kinda resemble forcing you to at least bind variables to the outputs but there’s nothing really like the linear types that we want and deserve | ||||||||||||||||||||||||||||||||
▲ | aiono 2 days ago | parent | prev [-] | |||||||||||||||||||||||||||||||
Simple, your compiler/type checker/static analyzer/whatever needs to ensure that any value that is not unused gives an error. But you need some sort of static analysis for that. Without type checking you don't do that of course. |