Almost every Java project I’m developing depends on the Vavr library. While I enjoy working with monadic types, sometimes I spot challenging code and wonder how to write it, so it is readable.

One such situation is combining values held by two or more monadic types into a single object. While the task looks very simple, it has some quirks.

Note: This blog was originally posted on Softwaremill’s blog


The use case

Consider the following example. We want to manufacture a car which is composed of three parts: engine, wheels, and a body. We can only assemble a vehicle when we have all the elements in place. To pick them up from a depot, we need to call three dedicated methods:

private Option<Body> body() {
 return Some(new Body());
}
private Option<Wheels> wheels() {
 return Some(new Wheels());
}
private Option<Engine> engine() {
 return Some(new Engine());
}

As you can guess, if there is a part in a depot, we receive it wrapped in an instance of theSome class; otherwise, it is a None.


Let’s combine

How to consolidate them together, so we have a new shiny car? We can apply monad transformations and do the following:

final Option<Car> ourShinyCar =
body().flatMap(
b -> engine().flatMap(
e -> wheels().map(
w -> new Car(b, e, w)
)
  )
 );

Yay! It works! But can you read it easily? There is a lot of braces, arrows, and mapping of the stuff around. Maintaining such a creature can be annoying.


Another way

What is the alternative? In Scala, we have a nice solution for such situations. We could apply one of the basic building blocks of this language, which is the for-comprehension construct:

var car = for {
 engine <- engine()
 wheels <- wheels()
 body <- body()
} yield Car(body, engine, wheels)

The construct above is more lightweight and is easier to understand, in my opinion. But what about Java? Surprisingly, we can enjoy a similar structure here too! The Vavr library offers its implementation of for-comprehension. When we apply this mechanism to our situation, the code changes to the following:

final Option<Car> car = For(
 engine(),
 wheels(),
 body()
).yield(
 (engine, wheels, body) -> new Car(body, engine, wheels)
);

You may wonder, in what circumstances the construction is worthwhile to apply? I would adopt when it’s cheap to compute, individual values used in a for-comprehension. The necessity of providing monads/iterables that must be already computed when passing them to the For(...) method is the most important concern in the construct. In our example, it means we have to execute all methods picking up parts from a depot first.

On the other hand, when it is quite expensive to compute values to combine them, e.g. calling an external service or querying a database, maybe using the flatMap method is a better option. In case of any error, while fetching one value, we will not compute the other ones.

Supplying values

A solution to this would be providing yet another variant of the for-comprehension in the Vavr library, accepting instances of java.util.function.Supplier. Considering the example, we are talking about, it could look like this:

private static <T1, T2, T3> ForSupplier3Option<T1, T2, T3> For(Supplier<Option<T1>> ts1,
Supplier<Option<T2>> ts2,
Supplier<Option<T3>> ts3) {
 return new ForSupplier3Option<>(ts1, ts2, ts3);
}
 
public static class ForSupplier3Option<T1, T2, T3> {
private final Supplier<Option<T1>> ts1;
  private final Supplier<Option<T2>> ts2;
  private final Supplier<Option<T3>> ts3;
private ForSupplier3Option(Supplier<Option<T1>> ts1, Supplier<Option<T2>> ts2, Supplier<Option<T3>> ts3) {
this.ts1 = ts1;
 this.ts2 = ts2;
 this.ts3 = ts3;
  }
 
  public <R> Option<R> yield(Function3<? super T1, ? super T2, ? super T3, ? extends R> f) {
  Objects.requireNonNull(f, "f is null");
  return
  ts1.get().flatMap(t1 ->
  ts2.get().flatMap(t2 ->
  ts3.get().map(t3 -> f.apply(t1, t2, t3))));
  }
}

However, this is not a perfect solution either. The above solves a single case of three suppliers. What about other cases for one, two or even more of them? The next question is about other than Option types returned from suppliers like Try or Iterable. When we consider this, the obvious answer seems to be generating them instead of writing such an amount of code manually. But does the gain of having such variety exceeds the cost of maintaining it? There is no easy answer to this 😉

Combine on request

Another application of Vavr’s for-comprehension could be data permutations. We can do this by using a variant that works on java.lang.Iterableinstances. Note that io.vavr.Value extends it so we can use all Value’s implementations.

By applying for-comprehension, we can compute combinations of data from multiple sources lazily, because the result is an iterator. It is an excellent way to generate data, for example for testing purposes.

// List.zip/zipWith
final Option<String> option = Option.of("value");
final Either<String, String> either = Either.right("yay");
final Iterator<Integer> integers = Iterator.tabulate(5, value -> value + 1);
final List<Tuple2<Integer, String>> zipped = integers.zip(either);
final List<Tuple3<Integer, String, String>> zipped3 = zipped
.zip(option)
  .map(e -> Tuple.of(e._1._1, e._1._2, e._2));
//the same with for-comprehension
final Iterator<Tuple3<Integer, String, String>> combined = API.For(
integers,
  either,
  option
)
  .yield(Tuple::of);

For the above example, I have three remarks. The first one is when calling List.zip/zipWith you end up with a collection that is eagerly computed while for-comprehension provides you with an iterator.

The second remark is about code that is easy to read. Combining more than two Iterable instances by zipping them looks not as clean as the other possibility. It is good enough when you have a list and some other iterable, but for a situation with more collections, I would go with for-comprehension.

The second remark is about code that is easy to read. Combining more than two Iterable instances by zipping them looks not as clean as the other possibility. It is good enough when you have a list and some other iterable, but for a situation with more collections, I would go with for-comprehension.

And the last, third note is the fact that you have zip/zipWith methods for io.vavr.collection.List implemented. Such situation narrows its usability to specific circumstances. For-comprehension has a more flexible API. We can use it even if we have no List instance in hand.


Pros and cons

In my opinion, the key and the first benefit of applying for-comprehension is cleaner code that is not only easier to read, but also to maintain.

It allows even to mix various types since for-comprehension works on java.lang.Iterable. Support of Try/Future/Option types added in v0.10.0 makes it even more useful.

On the cons side or nice to have things, I can list no support for filtering similar to the one in Scala and no versions of the for-comprehension accepting lambdas or suppliers. Without such features, the Vavr’s construct can be sometimes seen as a poor relative to the one in Scala.


Wrap up

Considering the pros and cons of the construction, we can easily see the limitations of the Java syntax compared to the Scala. While the latter was designed to have such feature out of the box, the former does not even support Try or Either in the standard library.

Next, it is hard to compare features of both for-comprehensions, but Java’s version is nothing to be ashamed of, I think. Having it in your toolbox when using the Vavr library is a good thing and can be extremely helpful in some situations.

Do you have other experiences or thoughts about for-comprehensions? Or maybe do you agree with me? Let me know and leave a comment!

Photo by Clément H on Unsplash

Categories: programming

0 Comments

Leave a Reply

This site uses Akismet to reduce spam. Learn how your comment data is processed.