DEV Community

Sergiy Yevtushenko
Sergiy Yevtushenko

Posted on • Updated on

How Interfaces May Eliminate Need For Pattern Matching (sometimes)

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
}
Enter fullscreen mode Exit fullscreen mode

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
}
Enter fullscreen mode Exit fullscreen mode

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
}
Enter fullscreen mode Exit fullscreen mode

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)
        },
    }
}
Enter fullscreen mode Exit fullscreen mode

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
            },
        }
    },
}
Enter fullscreen mode Exit fullscreen mode

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...));
Enter fullscreen mode Exit fullscreen mode

Adding more operations is just as easy:

    operation1(args...)
        .flatMap(value1 -> operation2(args...))
        .flatMap(value2 -> operation3(args...))
        ...
        .flatMap(valueN-1 -> operationN(args...));
Enter fullscreen mode Exit fullscreen mode

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)

Collapse
 
alekseiberezkin profile image
Aleksei Berezkin

Thanks for sharing. Could you please provide some examples (i.e. kinds of operations) that fit your last flatMap example?

Collapse
 
siy profile image
Sergiy Yevtushenko

It can be any function which may succeed or fail depending on conditions.

Following example is taken from real project code:

    private static Optional<Instant> parseInstantCursor(JSONObject params) {
        return safeString(params, "cursor")
            .toOptional()
            .flatMap(source -> Optional.of(source.split(":"))
                .filter(v -> v.length == 2)
                .flatMap(HighLevelApiHandler::parseInstant));
    }

    private static Optional<Instant> parseInstant(String[] pair) {
        return allOf(parseLong(pair[0]).filter(v -> v > 0), parseInt(pair[1]).filter(v -> v >= 0))
            .map(Instant::ofEpochSecond);
    }

    private static Optional<Long> parseLong(String input) {
        try {
            return Optional.of(Long.parseLong(input));
        } catch (NumberFormatException e) {
            return Optional.empty();
        }
    }
Enter fullscreen mode Exit fullscreen mode

This example also shows how looks new coding style mentioned at the beginning of the article.