what is the best practice (pure functionally speaking) for terminating the application in case of an error if throwing exceptions is not recommended? shall i log the error and use sys.exit(n) anytime i need to terminate the application?
Use exceptions for exceptional situations. Rule of thumb, if your application canât recover from an error, throw an exception and let it crash. If it can recover, then do that.
Examples of recovering are using Option, Either, or other sum types to indicate error situations.
From the functional perspective, exceptions invalidate referential transparencyâi.e. you canât directly substitute a piece of exception-throwing code for the value it could evaluate to, because that would change the meaning of the code. So generally we try fairly hard to not throw exceptions.
An exception (âŚ) is performance bottlenecks. If youâre profiling your code and finding an unacceptable slowdown from creating lots of âresultâ-style objects, you may need to rewrite that part of the code to throw/catch exceptions instead. But, this should be a last resort.
If weâre talking about a monolithic, single-pass application, you would
surround the main application calculation with something that would react
to failure values:
object MyApp {
def main(args: Array[String]): Unit = {
Try {
//âŚmain application calculationâŚ
} match {
case Failure(problem) =>
System.exit(-1);
case _ =>
//âŚnothing to do here, just let application quitâŚ
}
}
}
But itâs important to realize that you canât be functional âall the way
downâ. The essence of functional programming is that you are expressing
your application as a big functional equation/expression, and by definition
the perfect such expression would have no side-effects. Zero. This mean
youâd also have no I/O, and no permanent side-effects whatsoever.
This dream I think is theoretically impossible, since at a minimum you must
change the quantum state of the universe in some minimal way in order to
produce any form of information that you didnât already have, and that
quantum state change would be a side-effect. That is, you need to at least
warm up your CPU a tiny bit to compute anything, and that temp diff would
be a side-effect to a purist.
Just be aware that you have to be at least a little bit side-effecting and
non-functional at the edges of your application.
Not a âone fits allâ solution there, obviously, but lots of food for thought.
Given your specific question, Iâd think it just boils down to the necessity of the ânewsâ of the failure having to bubble up to the main method one way or the other. Devising a concrete strategy for which (combination of) vessel(s) to use (Exceptions, Either,âŚ) for this news, and when to switch between them, is the hard part.
sys.exit() wonât let anybody intercept - some piece of code way down in the call stack may decide to pull the plug without warning for everything thatâs going on in the system. That would be my very last choice to consider. All the other options just signal that something went wrong, and higher level code might jump in and mitigate the problem, or at least do some cleanup work - until the news ends up in main, which can pull the plug by just returning.
This seems like a no-op. If an exception bubbles up to the main method, it will cause a stack trace and program exit anyway.
More generally, exception handliers should work on as small a slice of code as possible. They should definitely not be wrapping your entire app and catching any possible exception that got thrown.
I think youâve misunderstood functional programming. Itâs perfectly possible to be purely functional and do I/O without side effects. See e.g. Haskellâs IO type, or There Can Be Only One...IO Monad â John A De Goes
When encountering an exception you have two general options:
1- Recover/Ignore/Handle it (following your use case)
2- Throw it and end the program
In both cases you want to handle/throw it in the most sensible way:
1- Keeping the handling logic contained in a specific and appropiate
functionality module of your code (i.e. one that can send messages to
users/admins)
2- Keep outside world puntually informed (users/admin), generally letting a
program crash is considered a bug
Functional structures like (but not limited to) monads just provide more
appropiate/standarized/easy to reason semantics, as you can pass the
exception around as a value while mantaining your program fully functional
(referentially transparent) until the âend of the worldâ (main) in which
you allow the side effects to kick in, effectively âopeningâ your
application to the outside world (unsafePerformIO and company)
Nope. You can do I/O just fine without side effectsâyou just need to wrap them and turn them into first-class effects. First-class effects make I/O pure.
Yeah, but that doesnât invalidate functional purity. Haskell essentially does the same thingâit runs the entire program, which is an IO () value, in its runtime.
Look, what do you mean when you say âpureâ? What I mean is âreferentially transparentâ, meaning you can substitute an expression for the value it evaluates to, without changing the meaning of the program. And this includes stuff like throwing exceptions.
By this definition you are pure if you control your effects using the IO type (âmonadâ is irrelevant here, itâs just a composition mechanism).
What do you mean by âcan substituteâ? Would you still get the same I/O if
you substitute an expression that creates an IO object by that IO object?
I mean literally substitute directly in the source code. E.g., substitute this:
val result = IO { println("Hi!"); 1 + 1 }
for this:
val result = IO { println("Hi!"); 2 }
IO is deliberately designed so that you canât type in a literal âIO objectâ, hence I gave examples of evaluating stuff inside IO creation expressions.
If you donât have that IO wrapper around the printing and then addition, then you are changing the meaning of the program because now itâs a program that can potentially throw exceptions. IO programs by design wonât.
Purely functional means I can substitute an expression by the value it
produces. If I canât create such a value without the expression, then it is
clearly not purely functional.
Evaluating the same expression giving you something of type IO[A] twice, or passing the result of the expression is the same thing. Thatâs referential transparency, and something you canât do with side effects.
It terminates the main thread by default, it does not kill the JVM. This becomes important when you have more than one non-daemon thread. In these cases it makes sense to have an explicit exception handler that prints the stack trace and terminates the JVM.
What is with a Future? Futures use a Try based exception error handling. Should that be avoided too? Should I wrap an Either with a custom error type into a Future to avoid exceptions? In my opinion an Exception is firstly an error type like any other custom error type. So long If you do not throw it and handle it with the appropriate Scala error type like Future, Try or Either then I think nothing speaks against the use of exceptions. I know that the consensus of the topic is to avoid throw exceptions. I just wanted to point out that exceptions are not bad per se.
There are some problems with exceptions per se: No sealed hierarchy, construction is costly,⌠Using Futures as vessels for exceptions comes with additional issues: You cannot declare the type(s) of exceptions you actually expect, exceptions are easy to get lost/go unhandled in âfire and forgetâ Futures,⌠(Most of these issues are touched in the 47 Degrees slides linked in the OP.) For these reasons, Iâd be inclined to use Future[Either[E, T]] in most situations, at least when arriving at module/API boundaries.
âŚbut then, even after years Iâm still experimenting with failure handling strategies, and as a Haskell learner Iâm stumbling over the same questions - IO (Either e t) or just IO tâŚ?! This is hardâŚ