How to use monads in Java

Przemysław Sobierajski

Java is not a functional programming language. Despite the fact that Java 8 came with some elements from functional world, there is still no way to write fully functional code in Java. One of the key aspect of functional programming is using monad. I am not going to describe what are monads but I will describe some of them and respond to the title question.

How to use monads in Java? Simply, add Vavr to dependencies of your project:

<dependency>
    <groupId>io.vavr</groupId>
    <artifactId>vavr</artifactId>
    <version>0.9.2</version>
</dependency >

Vavr is a library for Java that provides persistent data types and functional control structures. It gives you set of immutable Values (monadic containers) which can help with writing functional code in Java. Concepts of this monads is taken from a functional programming languages like Scala.

Option

Option is an interface which represents an optional value. Instances of Option are either an instance of Some or the None. Some contains a value (which can be even null) and None is a singleton representation of the undefined Option. It adheres to the requirement of a monad to maintain computational context when calling map(). This means that calling map() on a None will result in a None, and calling map() on a Some will result in a Some.

You may have heard that java.util.Optional is broken. I do not want to go into details. Java’s Optional does not satisfy all monads laws. You can read more at: https://developer.atlassian.com/blog/2015/08/optional-broken/. In the Java Optional the context can be changed from Some to None when we call:

optional.map(someString -> (String) null )

In Vavr, the Option works like an Option from Scala. Besides that differences, Option has much richer API than Optional.

@Test
void optionVsOptional() {
    //java Optional
    Optional<Object> optional = Optional.of(1).map(a -> null);
    //vavr Option
    Option<Object> option = Option.of(2).map(a -> null);
    Option<Object> emptyOption = Option.none();

    assertThat(optional.isPresent()).isFalse();
    assertThat(option.isDefined()).isTrue();
    assertThat(option.get()).isNull();
    assertThat(emptyOption.isDefined()).isFalse();
}

Try

Try is a type which represents a computation that may either result in an exception, or return a successfully computed value. Instances of Try, are either an instance of Success or Failure. You do not have to deal with checked exceptions anymore. It enables writing Java code without catch clause. I will write more about that in the next article.

int countLinesInFile() throws FileNotFoundException {
    throw new FileNotFoundException();
}

@Test
void javaTryCatch() {
    try {
        int lines = countLinesInFile();
        Assertions.fail();
    } catch (FileNotFoundException e) {
        e.printStackTrace();
    }
}

@Test
void vavrTry() {
    Option<Integer> lines = Try.of(this::countLinesInFile)
            .onFailure(Throwable::printStackTrace)
            .toOption();
}

Lazy

Lazy is a type which represents a lazy evaluated value. Lazy is using memoizaition so it should be used with pure functions only. Wrapped method is called when you need a result.

@Test
void lazy() {
    Lazy<Double> randomLazyValue = Lazy.of(Math::random);

    assertThat(randomLazyValue.isEvaluated()).isFalse();

    assertThat(randomLazyValue.get()).isEqualTo(randomLazyValue.get());

    assertThat(randomLazyValue.isEvaluated()).isTrue();

Either

Either represents a value of two possible types. It is like Tuple which store two different types but it only can have one value at the same time. An Either is either a Left or a Right. It can be used as a type which is returned from a function in case you want to distinguish between success and failure of the function call. Then you can perform some action on success and another one on failure. By convention the success case is Right and the failure is Left.

Future

Future wraps some computation and provides a set of non-blocking operations on computation result. It has either a pending or a completed state. Pending means that a computation is ongoing and it allows you to cancel a computation before it changes state to completed. Completed state means that the computation finished successfully with a result, failed with an exception or was cancelled. The main advantage over the core Java Future is that we can easily register callbacks and compose operations in a non-blocking way.

private int sleepAndThrowExc() throws InterruptedException {
    Thread.sleep(300);
    throw new IllegalArgumentException();
}

private int sleepAndReturnResult() throws InterruptedException {
    Thread.sleep(100);
    return 1;
}

@Test
void future() {
    Future<Integer> catnap = Future.of(this::sleepAndThrowExc)
            .andThen(res -> System.out.println("1: " + res))
            .andThen(res -> System.out.println("2: " + res))
            .fallbackTo(Future.of(this::sleepAndReturnResult))
            .andThen(res -> System.out.println("3: " + res))
            .andThen(res -> System.out.println("4: " + res))
            .onFailure(throwable -> System.out.println("FAIL"))
            .onSuccess(res -> System.out.println("SUCCESS: " + res));

    assertThat(catnap.isCompleted()).isFalse();
    Integer result = catnap.getOrElse(-1);
    assertThat(catnap.isCompleted()).isTrue();
    assertThat(result).isEqualTo(1);

    // The output will be:
    // 1: Failure(java.lang.IllegalArgumentException)
    // 2: Failure(java.lang.IllegalArgumentException)
    // 3: Success(1)
    // 4: Success(1)
    // SUCCESS: 1
}

Summary

In this article I have described some monadic containers from Vavr library. They may look like syntactic sugar but beside that they have much more rich API than Java equivalents and they are really well designed. Java is still not a functional language but with Vavr you can write your programs in more functional way.

Poznaj mageek of j‑labs i daj się zadziwić, jak może wyglądać praca z j‑People!

Skontaktuj się z nami