As every Java developer know, variables with non-primitive types are initialized to null
. This is the Java (and not just Java) way to say that "value is missing" or "variable has no value assigned yet".
There are often situations when we may return value, but sometimes we need to notify the caller that there is an error. The idiomatic Java way to do this is to throw an exception.
What described above situations have in common? They cover different cases and use different ways to represent/handle them.
In fact, both of them deal with special states of the variables. There is also a third special case, as you'll see below.
Variable Special States
What is the variable special state? The special state of the variable is the state, when the value of the variable is not (yet) available for some reason.
There are three special states of the variables:
- Variable value is missing
- Operation may return a value or report an error, so a value might be present or not
- Variable holds a result of the asynchronous operation which is still in progress, so the value is not yet available.
Let's take a look how these cases handled traditionally.
Missing Value
The traditional Java way to represent missing is to assign a null
to the variable. This approach has several drawbacks and is a source of a huge number of errors.
Funny enough, even Java itself does not threat such variables as real ones, with real type - instanceof
check for such variables returns false
.
Return Value or Report Error
The traditional way in Java to report failure is to throw an exception. Since Java uses dedicated syntax to throw and catch exceptions, often this case is hard to even see as a special state of some variable. Needless to say, that exception handling syntax is verbose, hard to compose and quickly gets unreadable in non-trivial error handling cases.
But this is only part of the issue. As you already know, there are two types of exceptions - checked and unchecked.
Checked exceptions pollute the code with boilerplate, although, at least, provide information which exceptions can be thrown. Language also enforces handling or propagating them, so, in theory, they should result in more reliable code. Unfortunately, verbosity makes them so inconvenient, that developers tend not to use them or wrap into unchecked exceptions.
Unchecked exceptions keep code less verbose, but the price for this is the complete lack of support from the compiler side. Such exceptions too easy to overlook. Many seasoned developers have a habit of looking into the body of the invoked method to make sure that it does not throw an unchecked exception. This extra navigation and code reading slows down the process and causes mental overhead.
Not Yet Available Value
Since version 8 Java has built in support for handling this case - CompletableFuture
. Built on FP-inspired concepts, it uses the following approach to handle not yet available value: instead of letting user code access the value, CompletableFuture
has a number of methods, which accept functions (lambdas) as a parameter. The invocation of these functions are postponed until value is available.
Notice, that this approach is quite flexible and does not require any special support at the language level.
Finding Consistent Approach
Even though an idiomatic approach to each case is different, intuitively looks like they should have similarities.
Functional Programming provides a convenient tool to bring all these cases under the common umbrella - monads.
Each special state corresponds to a particular monad:
- Option/Optional/Maybe monad handles the missing value case
- Result monad handles the value/error case
- Promise monad handles the case of not yet available value
Monads leverage the Java type system to represent special states, and this provides numerous immediately sensible advantages:
- All special states are immediately visible in the code. There is no need to spend time navigating and reading code to make sure that it does/don't return
null
or throw an exception. - Significantly reduced boilerplate code and improved code readability. All repetitive checks like
if (variable != null)
disappear and developer can write code as if there is only "happy day scenario", leaving final decision to the caller. - Compiler is your best friend, which makes sure that every single special case is covered and properly handled.
In the long run, there are more subtle, but no less valuable advantages:
- Together with the Java 11
var
declaration and type inference, monad-based code has almost all redundant type declarations shaved off. - There is a natural tendency to break code into small, generalized, reusable, easy to read and understand, focused on single task functions.
- Reduced boilerplate makes business logic much more visible in code, easier to read and understand. Overall code base looks and feels more high level.
- Significantly reduced number of bugs, especially low-level ones like missing
null
check or unhandled exception. - Code is simpler to test, usually less mocking is necessary.
- Handling of special states happens transparently and for typical backend code the whole call chain can be easily navigated back and forth. There is no hidden execution paths like dedicated exception handlers which have no visible link to the code, but may affect the resulting response.
- Significantly reduced mental overhead, which enables deeper focus on high level business logic and significantly improves productivity.