For about a year, all Java code I'm writing heavily uses functional programming approaches. This provides a lot of benefits, but description of this new style is a topic for a separate long article.
Now I'd like to focus on one curious observation: sometimes OO provides a more convenient way to use even purely FP concepts like monads.
Short Introduction
Monads is a very convenient design pattern, often used to represent special states of values - potentially missing values (Maybe
/Option
) or results of computations which may fail (Either
/Result
).
Usually such a monad can be implemented using Algebraic Data Types, in particular, Sum Types.
To illustrate the concept, let's imagine that we are implementing Java 8 Optional
from scratch:
public interface Optional<T> {
<U> Optional<U> map(Function<? super T, U> mapper);
<U> Optional<U> flatMap(Function<? super T, Optional<U>> mapper);
... //other methods
}
Now we need two implementations. One will handle the case when we have no value:
public class None<T> implements Optional<T> {
public <U> Optional<U> map(Function<? super T, U> mapper) {
return new None<>();
}
public <U> Optional<U> flatMap(Function<? super T, Optional<U>> mapper) {
return new None<>();
}
... // other methods
}
Another one will handle the case when we have value:
public class Some<T> implements Optional<T> {
private final T value;
... //constructor, etc.
public <U> Optional<U> map(Function<? super T, U> mapper) {
return new Some<>(mapper.apply(value));
}
public <U> Optional<U> flatMap(Function<? super T, Optional<U>> mapper) {
return mapper.apply(value);
}
... // other methods
}
From the practical standpoint, any Optional<T>
in our application will always be an instance of either Some<T>
or None<T>
, unless we add more implementations (sealed classes in Java 16+ solve this issue). In other words, Optional<T>
is a sum of types Some<T>
and None<T>
.
How About Pattern Matching?
As mentioned above, algebraic data types and monads are concepts which are widely used in FP. In order to handle different cases (for example, None
and Some
like shown above), Functional Programming uses pattern matching. In practice, it means that the received value is checked for type and then handled accordingly. For example, this is how such a situation is handled in Rust:
fn try_division(dividend: i32, divisor: i32) {
match checked_division(dividend, divisor) {
None => println!("{} / {} failed!", dividend, divisor),
Some(quotient) => {
println!("{} / {} = {}", dividend, divisor, quotient)
},
}
}
This looks clear, readable and convenient. The problem appears when we need to perform more than one operation on the returned value AND that operations also may return Optional
. Now we have something like that (let's continue using Rust-like syntax here):
match operation1(args...) {
None => ...,
Some(value1) => {
match operation2(args...) {
None => ...,
Some(value2) => {
//do more work
},
}
},
}
As you can see, pattern matching works perfectly fine and compiler makes sure that programmer checks all possible cases every time. Needless to say, how good this for the code reliability. But writing and reading such a code is a pain.
Many FP languages adopted so called do
syntax to make handling such cases much more concise and readable, but this is another (long) story and source of heated debates.
Instead, I propose to look at another solution proposed by OO languages, in particular Java.
Unified Interface
As you, probably, already noticed, in case of Java both classes implement the same interface. This means that we can just call instance methods without checking for particular type every time:
operation1(args...)
.flatMap(value1 -> operation2(args...));
Adding more operations is just as easy:
operation1(args...)
.flatMap(value1 -> operation2(args...))
.flatMap(value2 -> operation3(args...))
...
.flatMap(valueN-1 -> operationN(args...));
It does not matter how many operations we need to compose together, code will remain concise, easy to read, write and maintain.
Conclusion
Of course, the curious observation shown above does not mean that one language/languages/paradigm/etc. is better than the other. Instead, I'm trying to show that we, as programmers, should have our minds open, as the best solution for the problem may come from an unexpected direction.
Top comments (2)
Thanks for sharing. Could you please provide some examples (i.e. kinds of operations) that fit your last flatMap example?
It can be any function which may succeed or fail depending on conditions.
Following example is taken from real project code:
This example also shows how looks new coding style mentioned at the beginning of the article.