3 ms·
The only thing that gets to make that decision is the call frame that initiated the program execution. Meaning -- only fn main gets to return stuff to the user.
by preseinger 4y ago
The only thing that gets to make that decision is the call frame that initiated the program execution. Meaning -- only fn main gets to return stuff to the user. Everything else, all other functions, should (almost always) express faults as returned errors.
- taeric 4y agoIf you are writing a library, sure. If you are designing a system, this is silly. Functions aren't written in isolation. Such that there can be a broad definition of all exceptions that are expected to go to a user. In the full system, you will have many side channels of data flow. Metrics and logging are also worth considering. If you really want to boil a system down to a single entry point, at least do it like lisp, where the system has pluggable restart conditions defined.
- preseinger 4y agoFunctions should absolutely be written in isolation. That's the point of functions! If I see this line of code result = val.method(foo, bar) I make certain assumptions. Primarily that the implementation of val.method will in general only interact with foo, bar, and the fields of val. Metrics and logging should be dependencies injected to the val struct, not globals; expedience sometimes requires us to take shortcuts, sure, but those are exceptions, not patterns. More fundamentally, that line of code, as it exists on the page, expresses an unambiguous flow of execution. Callers provide specific inputs to a function, the function goes off to do _something_ in a new level of the call stack, and, importantly, returns a result to me. Then I continue to the next thing. The call stack underneath that function can be arbitrarily huge, sure. But I should be able to trust that when I read a sequence of expressions in my source code, the flow of execution through those expressions is exactly what it appears to be. Exceptions subvert this intuition. They make it so I can't know, or even predict, what will happen to my flow of execution when I jump into a new call stack. Every expression is potentially a return statement. That means I have to read the implementation of every function I call, recursively, in order to understand the execution flow of my code. This isn't tractable. We obviously have to make some affordances for major failures, OOMs, div by 0, etc., so this isn't an absolute rule. But these have to be exceptional cases, not something that programmers need to consider as a matter of course. We simply don't have the cognitive capacity to model behavior in terms of recursively and arbitrarily complex implementation details. We need to be able to ignore this level of detail via simple and consistent abstractions.
- taeric 4y agoSimple functions, certainly. But, not all things can be simple. Consider your accelerator pedal in a car. Somewhat simple method to increase fuel to the engine. If something is wrong with the engine, it doesn't message that back to you through the pedal. Even though the pedal won't work. This can be as trivial as the car not being on, the pedal will not work. So, if you are picking the straw man where every function has side effects and communicates back through a side channel, I agree. But if you are building a system where some things would require way more effort and code to get what you are aiming for, then we are in the realm of this article, where a panic that sends it back to the user makes far more sense.
- preseinger 4y ago> Consider your accelerator pedal in a car. Somewhat simple method to increase fuel to the engine. If something is wrong with the engine, it doesn't message that back to you through the pedal. Even though the pedal won't work. This can be as trivial as the car not being on, the pedal will not work. I don't see how this is a problem? The accelerator pedal API, so to speak, is purely mechanical -- its "return value" is whether or not you were able to put it to the position you wanted, it doesn't promise to return any information about the downstream effects of that pressure/setting. The accelerator is tightly coupled to the valve which controls how much fuel is given to the engine, but anything after that is a downstream (event-driven?) effect that I as a driver have to observe through other means. So if the valve is stuck I expect to learn about that failure after I call this fn, sure. But the impact on the intake, or the engine RPM, or the transmission, or my wheel speed, or my actual velocity, these are all things I discover through other means. So it's not fn depress_accelerator(f64 amount) -> CarState result but rather something like fn depress_accelerator(f64 amount) -> bool worked fn depress_accelerator(f64 amount) -> f64 throttle It's not complicated. When I call a function, I expect it will do what its signature says it can do, and I expect it to return control flow to me. If you can't rely on these assumptions, it's effectively impossible to build reliable software.
- taeric 4y ago