Hacker News new | past | comments | ask | show | jobs | submit login

The thing with functional programming (specifically, immutable data,) is that as long as the invalid state is immutable, you can just back up to some previous caller, and they can figure out whether to deal with it or whether to reject up the its previous caller.

This is why Result (or Maybe, or runExceptT, and so on in other languages) is a perfectly safe way of handling unexpected or invalid data. As long as you enforce your invariants in pure code (code without side effects) then failure is safe.

This is also why effects should ideally be restricted and traceable by the compiler, which, unfortunately, Rust, ML, and that chain of the evolution tree didn't quite stretch to encompass.






Say a function has some return type Result<T, E>. If our only error handling mechanism is Err(e) then were restricted to E representing the set of errors due to invalid arguments and state, and the set of errors due to the program itself being implemented incorrectly.

In a good software architecture (imo) panics and other hard failure mechanisms are there for splitting E into E1 and E2, where E1 is the set of errors that can happen due to the caller screwing up and E2 being the set of errors that the caller screwed up. The caller shouldn't have to reason about the callee possibly being incorrect!

Functional programming doesn't really come into the discussion here - oftentimes this crops up in imperative or object oriented code where function signatures are lossy because code relies on side effects or state that the type system can't/won't capture (for example, a database or file persisted somewhere). Thats where you'll drop an assert or panic - not as a routine part of error handling.


You shouldn't pass invalid values to a function. If a function can return some sensible value for some input, then the input is not invalid - even if the return type is an error by name.

Ideally, you can constrain the set of inputs to only valid ones by leveraging types. But if that's not possible and a truly invalid input is passed, then you should panic. At least that's the mental model that Rust is going with.

You do lose out on the ability to "catch" programming errors in subcomponents of your program. For example, it's extremely useful to catch exceptions related to programming errors for called code in response to a web request, and return a 500 in those cases. One could imagine a "try" "catch" for panics.

The thing is, it takes a lot of discipline by authors to not riddle their code with panics/exceptions when the language provides a try/catch mechanism (see C# and Java), even when a sensible error as value could be returned. So Rust opts to not introduce the footgun and extra complexity, at the expense of ungraceful handling of programming errors.


> Ideally, you can constrain the set of inputs to only valid ones by leveraging types. But if that's not possible and a truly invalid input is passed, then you should panic.

But how can the caller know what is "a truly invalid input"? The article has an example: "we unfortunately cannot rely on panic annotations in API documentation to determine a priori whether some Rust code is no-panic or not."

It means that calling a function is like a lottery: some input values may panic and some may not panic. The only way to ensure that it doesn't panic is to test it with all possible input values, but that is impossible for complex functions.

It would be better to always return an error and let the caller decide how to handle it. Many Rust libraries have a policy that if the library panics, then it is a bug in the library. It's sad that the Rust standard library doesn't take the same approach. For println!(), it would mean returning an error instead of panicking.


The program can detect invalid state, but your intention was to never get to that state in the first place. The fact that the program arrived there is a Logic error in your program. No amount of runtime shenanigans can repair it because the error exists without your knowledge of where it came from. You just know it's invalid state and you made a mistake in your code.

The best way to handle this is to crash the program. If you need constant uptime, then restart the program. If you absolutely need to keep things running then, yeah try to recover then. The last option isn't as bad for something like an http server where one request caused it to error and you just handle that error and keep the other threads running.

But for something like a 3D video game. If you arrive at erroneous state, man. Don't try to keep that thing going. Kill it now.


> The program can detect invalid state, but your intention was to never get to that state in the first place. The fact that the program arrived there is a Logic error in your program. No amount of runtime shenanigans can repair it because the error exists without your knowledge of where it came from.

True: a program can't fix an internal assertion error, and the failed component might not be recoverable without a reset. But that doesn't means that the whole program is doomed. If the component was optional the program might still work although with reduced functionality. Consider this: way you do not stop the whole computer if a program aborts.

As I mentioned elsethread, in unsafe languages an assertion error might, with high probabilty, be due to the whole runtime being compromised, so a process abort is the safest option.


> The fact that the program arrived there is a Logic error in your program.

No, your program correctly determined that user input was invalid.

Or your parser backtracked from parsing a Bool and decided to try to parse an Int instead.


That’s not invalid state. Your program correctly determined input is invalid.

Say user input is a number and can never exceed 5. If the user input exceeds 5 your program should handle that gracefully. This is not invalid state. It is handling invalid input while remaining in valid state.

Let say it does exceed 5 and You forget to check that it should never exceeds 5 and this leads to a division by zero further down your program. You never intended for this to happen and you don’t k ow why this happened. This is invalid state.

Now either this division by zero leads to an error value or it can throw an exception. It doesn’t matter. Do you know how to recover? You don’t even know where the bug is. A lot of times your don’t even know what to do. This error can bubble up all the way to main and now what do you do with it?

You crash the program. Or you can put in a fail safe before the error bubbles up to main and do something else but now (if your program retains and mutates that state) has invalid values in it and a known bug as well.

Imagine that input number represented enumeration values for movement of some robot. You now have one movement that doesn’t exist or was supposed to be something else. Thus if you keep your program running the robot ends up in an unexpected and erroneous place. That’s why I’m saying a program should crash. It should not be allowed to continue running with invalid state.

Like imagine if this was a mission critical auto pilot and you detect negative 45 meters for altitude. Bro crash and restart. Don’t try to keep that program running by doing something crazy in attempt to make the altitude positive and correct. Reset it and hope it never goes to the invalid state again.


> You crash the program.

No thank you.

> Or you can put in a fail safe before the error bubbles up to main and do something else

In other words,

>> your parser backtracked from parsing a Bool and decided to try to parse an Int instead

> but now (if your program retains and mutates that state) has invalid values in it and a known bug as well.

Unless,

>>> The thing with functional programming (specifically, immutable data,) is that as long as the invalid state is immutable, you can just back up to some previous caller, and they can figure out whether to deal with it or whether to reject up the its previous caller.


Your parser backtracking from a bool when it tried to parse an int is VALID behavior. Your parser tries to parse text into several possible types at runtime. Backtracking is STILL valid state. You’re not thinking clearly.

Look at my auto pilot example. You have a bug in your program you don’t know about. Altitude reads -9999 meters your heading and direction is reading at the speed of light. Your program sees these values and recognizes invalid state. There is a BUG in your program. Your program WAS never designed to go here.

You want to try to recover your autopilot program? How the fuck are you gonna do that? You don’t even know where the bug is. Does your recovery routine involve patching and debugging? Or are you gonna let your autopilot keep operating the plane with those nonsense values? That autopilot might hit the wind breaks to try to slow down the plane and end up crashing the thing.

You don’t recover this. You restart the program and pray it doesn’t hit that state again then when you are on the ground you debug it.

>>> The thing with functional programming (specifically, immutable data,) is that as long as the invalid state is immutable, you can just back up to some previous caller, and they can figure out whether to deal with it or whether to reject up the its previous caller.

This makes no fucking sense. Where the hell will you back up to? Your error came from somewhere but you don’t know where. Return an error value in the child function the parent function receives the error and returns an error itself and this keeps going until you bubble up to the main function because you don’t know where that error came from.

Now you have an error in the main function. Wtf are you gonna do? How do you handle an error in the main function that you have no idea where it came from? Here’s an idea. You have the main function restart the program loop. See a similarity here? It’s called crashing and restarting the program. Same effect!

This isn’t a functional programming versus non functional thing. It’s a concept that you’re not seeing.


I know this is not your point for the last paragraph, but have you read about the C-130 complete navigation system failure while trying land below sea level at the Dead Sea? :)

https://news.ycombinator.com/item?id=14409950


good one.

> This is why Result (or Maybe, or runExceptT, and so on in other languages) is a perfectly safe way of handling unexpected or invalid data.

They are great for handling expected errors that make sense to handle explicitly.

If you try to wrap up any possible error that could ever happen in them you will generate horrendous code, always having to unwrap things, everything is a Maybe. No thanks.

I know it is tempting to think "I will write the perfect program and handle all possible errors and it will never crash" but that just results in overly complex code that ends up having more bugs and makes debugging harder. Let it crash. At the point where the error happened, don't just kick the bucket down the road. Just log the the problem and call it a day. Exceptions are an amazing tool to have for things that are.. exceptions.


The monad and lifting fixes this problem of having to unroll the maybe type. But this is an advanced abstraction.

I actually disagree with this. Use the maybe type religiously, even without the monad because it prevents errors via exhaustive matching.

The exception should only be used if your program detects a bug.




Join us for AI Startup School this June 16-17 in San Francisco!

Guidelines | FAQ | Lists | API | Security | Legal | Apply to YC | Contact

Search: