Computational Expressions in Java
F# is an excellent language for writing type-safe code. One of its best features is computational expressions: syntactic sugar that makes code easier to read and error handling, among other things, effortless.
But what if you could bring that same magic to Java? While Java doesn’t have computational expressions built in, the same principles can simplify your code. Perhaps making it safer, prettier, and almost as fun as F#.
Result in F#
The Result type represents whether a computation succeeds or fails. It wraps a value of type ‘T in Ok on success, or type ‘E in Error on failure.
type Result<'T,'E> =
| Ok of 'T
| Error of 'E
With a computational expression, you can use Result like:
// A divide function that emits Error if the denominator is 0,
// or otherwise Ok wrapped around the result of a division
let divide x y =
if y = 0
then Error "Divide by zero"
else Ok (x / y)
// A computation expression of result,
// where wrapped context is hidden.
// The third fails and short-circuits!
let computation =
result { // result-expression inside {}
let! a = divide 10 2 // Ok 5, a is 5
let! b = divide a 3 // Ok 1, b is 5/3=1
let! c = divide b 0 // Error "Divide by zero"
return a + b + c // Not reached if any step fails.
}
// Pattern match the result as either a success or a failure.
// Will print Failure
match computation with
| Ok v -> printfn "Success: %d" v
| Error e -> printfn "Failure: %s" e
The nice thing with this construct is that the result of each step is available, so that we can sum it all up in the end: a + b + c
. This might feel obvious, but the computation short-circuits as soon as there’s an Error, like when dividing by zero. In the error case, the steps in the computation become obsolete, and possibly not suitable for use at all. And here these intermediates aren’t naturally available on error.
Instead of blowing up with exceptions or running into nested if statements, you get a graceful failure and the rest of the computation is skipped. The sum: return a + b + c
will not occur.
Whether the intermediate values are suitable to refer to or not obviously depends on the error category and domain. But this mechanism is surprisingly rare in mainstream languages. Most programming languages allow you to access these intermediate values without second thought. Unless an exception is thrown, which complicates the natural program flow. Wouldn’t it be better if these intermediate values were a tiny bit more tedious to access. Forcing you to have a second thought.
Without Computational Expressions
Without the computational expression we would probably write something nested to get the same effect.
It’s harder to get an overview in nested expressions, even though this code does the same thing as the earlier, as the code using the computational result expression.
// Same logic written as nested matches.
// Harder to read.
let computation =
let aResult = divide 10 2
match aResult with
| Error e -> Error e
| Ok a ->
let bResult = divide a 3
match bResult with
| Error e -> Error e
| Ok b ->
let cResult = divide b 0 // Will fail
match cResult with
| Error e -> Error e
| Ok c -> Ok (a + b + c)
// And pattern match the result. Will print Failure
match computation with
| Ok v -> printfn "Success: %d" v
| Error e -> printfn "Failure: %s" e
Some claim that this type of syntax-wrapped context is what makes F# such a great language. Computational Expressions can be very advanced and powerful, making code very elegant. Still just handling errors with elegance is enough in real world scenarios. Some call it railway oriented programming
Result in Java
Java doesn’t have Computational Expressions. But the same structure can be created in Java, using e.g. the Result type provided in previous blog post: Java without exceptions :
public sealed interface Result<T, E> permits Result.Success, Result.Failure {
record Success<T, E>(T value) implements Result<T, E> { }
record Failure<T, E>(E error) implements Result<T, E> { }
}
We can use pattern matching to show the result:
void show(Result<?,?> result){
print( switch (result) {
case Success(var a) -> "Success " + a;
case Failure(var e) -> "Failure " + e;
});
}
Usage of a Result expression, for solving the very same problem, structured as a computational expression, would in Java look like:
// A division that creates Failure when denominator is zero
Result<Integer, String> divide ( int a, int b){
return b == 0
? Result.failure("Division by zero")
: Result.success(a / b);
}
// A ResultExpression chain, mirroring the F# example.
var result =
ResultExpression.of(divide(10, 2))
.bind(a -> divide (a, 3))
.bind((a, b) -> divide(b, 0))
.yield((a, b, c) -> a + b + c);
show(result);
There is not much difference compared to the F# version.
In F#, the let!
means: take out the value from the context structure, the Result, and use that value to make a new context.
In Java, with ResultExpression
, we do this explicitly with bind
. Bind takes a function that receives the Success value, to compute a new Result context.
Java does not have the syntactic sugar of computational expressions. Instead ResultExpression
simply grows with each step, providing new values for each step. The first bind
sees: a
, the second sees: a, b
, the third: a, b, c
, and so on.
The sibling function yield
doesn’t accumulate steps. It returns a plain Result of a single value, like return
in the F# computation expression.
If something goes wrong, like the divide by zero in the second bind above, the expression will be short-circuited, and end up as Failure, caught in the pattern match of show.
The forwarding of the values from each step allows for graceful degradation under the hood. This is prettier with the language support in F#, as the values aren’t listed in each step, but it is as effective for code complexity in Java. You simply need to think twice before being tempted to use an intermediate value.
Is it worth it?
Is it worth providing functions with an increasing number of arguments per step? In practice it simplifies nested expressions, prevents stale values, and keeps the flow simpler.
With modern Java and pattern matching, the same expression as:
var result =
ResultExpression.of(divide(10, 2))
.bind(a -> divide (a, 3))
.bind((a, b) -> divide(b, 0))
.yield((a, b, c) -> a + b + c);
could be written without a computational expression like:
// Nested pattern matching is difficult to follow
var result =
switch (divide(10, 2)) {
case Result.Success(var a) -> switch (divide(a, 3)) {
case Result.Success(var b) -> switch (divide(b, 0)) {
case Result.Success(var c) -> Result.success(a + b + c);
case Result.Failure(var e) -> Result.failure(e);
};
case Result.Failure(var e) -> Result.failure(e);
};
case Result.Failure(var e) -> Result.failure(e);
};
Mapping without Failures
Some operations won’t fail. ResultExpression
provides map
to lift the calculated value into Success. Use it instead of bind
when success is the only path.
Other arithmetic operations are safe, so use map for those:
// Use map when it can't fail, and bind when it can
var result =
ResultExpression.ofValue(5)
.map(a -> a * 3)
.bind((a, b) -> divide(b, 0))
.yield((a, b, c) -> a + b + c);
show(result);
The ResultExpression actually uses many types, Steps, to mimic the language construct. It can be written as:
// Step by step, the types reveal themselves.
// Step1, Step2, Step3 show how types accumulate.
ResultExpression.Step1<Integer, String> x1 = ResultExpression.ofValue(5);
ResultExpression.Step2<Integer, Integer, String> x2 = x1.map(a -> a * 3);
ResultExpression.Step3<Integer, Integer, Integer, String> x3 = x2.bind((a,b) -> divide( b, 0));
// Yield the sum
Result<Integer, String> r = x3.yield((a, b, c) -> a+b+c);
show(r);
This is arguably easier to follow step-by-step, but much more verbose.
Each step provides its own type parameters. Using different types in each step is straightforward.
Here, a
is an integer, d
is double, while b
and c
are strings. The idea is to let each position represent the same value in every step. This means the arguments aren’t difficult to understand even as their number grows.
// Each step can use different types, here mixed int, string, double.
var result =
ResultExpression.ofValue(21)
.map(a -> "string " + a)
.map((a, b) -> String.format( "0x%x as %s", a ,b))
.bind((a, b, c) -> divide (a, (double) b.length()))
.yield((a, b, c, d) -> String.format("a=%d,b=%s,c=%s,d=%.2f and product is %.2f", a, b, c, d, a*d));
show(result);
Handling Exceptions
As we can see above, we can gracefully take care of exceptions using Result functions, like in divide. We can also use trial from Result, which wraps any exceptions in Failure, up front, especially handy for those scary divisions!
Here, trial simply runs the lambda in a try/catch block, converting any exceptions into Failure. It catches early and prevents extra code paths induced by exceptions, reducing code complexity.
// Trial wraps exceptions as Failure
var result =
ResultExpression.of(trial(() -> 10/2))
.map(a -> a*3)
.bind((a, b) -> trial(() -> b/0))
.yield((a, b, c) -> a + b + c);
// And pattern match on the corresponding Exception type as Failure
print( switch (result) {
case Success(var a) -> a;
case Failure(ArithmeticException e) -> "Math problems:" + e.getMessage();
case Failure(var e) -> "Some other failure:" + e.getMessage();
});
Sure, showing off these techniques with a simple calculation isn’t really fair. Computational expressions are tools for code that would otherwise become messy—especially in real-world scenarios, when things get gnarly fast.
Optionals in Java
Computational expressions work for Optionals as well. It’s a neat way to deal with many Optionals, which otherwise easily becomes complex.
// A computational expression on Optionals, instead of deep flatMaps.
print(OptionalExpression.of(getUserId(loginName))
.bind(userId -> getEmail(userId))
.bind((userId, email) -> getAvatarUrl(userId))
.yield((userId, email, avatarUrl) ->
String.format("User: %s, Email: %s, Avatar: %s", userId, email, avatarUrl))
.orElse("User info incomplete!"));
The equivalent without the OptionalExpression, with JDK, would be:
// A nested, harder-to-read tree
print(getUserId(loginName)
.flatMap(userId ->
getEmail(userId)
.flatMap(email ->
getAvatarUrl(userId)
.map(avatarUrl ->
String.format("User: %s, Email: %s, Avatar: %s", userId, email, avatarUrl)
)
)
)
.orElse("User info incomplete!"));
Wrapping Up
So, whether you prefer F#, Java, or just want to avoid waking up in a cold sweat over nested try/catch blocks, computational expressions can make your code safer, flatter, and a lot more readable.
This demonstrates that structure can be improved with computational expressions. Here we are merely toying around with simple calculation. Computational Expressions shine when they prevent complexity from emerging in real-world programs. And it can be done in Java by simply forwarding all values through the complexities that the context manages. Code nesting disappears.
Code that manages structure is code that you need to foster and adapt in your environment. You can’t do that when they belong to libraries that you don’t own. You don’t want to rely on libraries that change contract over time. Use it in your project and adapt it as you need.
Library code is someone else’s garden. Your error handling deserves homegrown tomatoes.