Redundancy of the Java language

(Note: the original title of this blog post was "For-Comprehensions In-Depth" but this post is more than a description of another Javaslang feature. I want to broadcast a message.)

We all are interested in good code quality. Pejorative phrases like Spaghetti code or Lasagna code describe different types of bad code quality. I've learned there even exists Ravioli code.

There are different responsibilities when it comes to writing good code.

The developer is responsible for using the language the right way. In general there are several paths that lead to a goal. Design patterns (object-oriented, functional or a combination of them) and idioms help the experienced developer to achieve the goal.

As always, less is more. A program is not finished when we can't add any more, it is finished when there is nothing that can be removed. It may take much effort and several refactorings to solve a problem in a few lines of code while staying comprehensible.

The language is responsible for being scalable by providing the developer with a small set of simple and powerful constructs that can be combined to create more complex constructs.

One of the worst things that can happen on the language level is redundancy. It is a clear sign that the right level of abstraction wasn't found. Mathematically we talk about the reflexive transitive closure that can be understood in a wider sense as minimal set of features that still form the language.

Have you ever heard of Pizza language?

Pizza

I made this picture while preparing a meal for my family. Each one of us likes the pizza in a different flavor. But it is not practical to make a different pizza for each person (depending on the number of persons).

This is how I expect the Java language architects must feel. The developers desire different features. Can these be added to the language such that the whole picture still makes sense? Does it taste well in the end?

🤔

I love languages that consist of a few simple building blocks that scale. During the last 20 years the Java language architects did an awesome job adding new features while staying backward compatible. But the increasing number of features tend to exist side-by-side.

Example: If-statement vs ternary operator ?:

// If-statement
int i;  
if (condition) { i = 1; } else { i = 0; }

// Ternary operator
int i = condition ? 1 : 0;  

If Java would have expressions instead of statements, life would be easier - no need for a ternary operator any more. Having ?: would be superfluous.

int i = if (condition) 1 else 0;  

Is it possible to change Java this way? I don't know. At least we would not need the return statement any more if we had expressions instead of statements. The syntax could look the same and Java would still be object-oriented. Sounds not too bad, right?

🤔

In our previous post we showed the Grill example, an application of Javaslang's for-comprehensions:

public static Option<Dinner> makeDinner(GrillService service) {  
    return
        For(service.getCharcoal(), charcoal ->
            For(service.getLighter(), lighter ->
                For(service.lightFire(charcoal, lighter), fire ->
                    For(service.getCornCob(), cornCob ->
                        For(service.grill(fire, cornCob))
                            .yield()
                    )
                )
            )
        ).toOption();
}

There are several pros using the for-comprehension compared to classical if-statements:

  • There is no risk to forget a logical branch. Unit-testing all possible combinations of if-branches gets exponentially harder with increasing depth. However, using Javaslang's property checking module would make it possible with just a few lines of code.
  • The whole computational logic is expressed with a functional DSL. It is an expression. There is no local state modified. It is hard to verify every possible execution of cascading statements that modify state.

But there is still something wrong with Javaslang's for-comprehension.

  1. It is redundant: For, For, ..., For, yield
  2. We see the Christmas Tree effect (prominent example: callback-hell), which makes it hard to read.
  3. The for-comprehension does not make use of the monadic property of the Option type. Instead we have to explicitly call toOption().

Scala's for-comprehension solves these issues by adding a special syntax on the language level (1. & 2.). Scala can solve 3. in a 'scalable' way because type constructors (aka higher-kinded types) make it possible to define a flatMap() method throughout the type hierarchy. The corresponding Scala code looks like this:

def makeDinner(service: GrillService): Option[Dinner] =  
  for {
    charcoal <- service.getCharcoal
    lighter  <- service.getLighter
    fire     <- service.lightFire(charcoal, lighter)
    cornCob  <- service.getCornCob
    dinner   <- service.grill(fire, cornCob)
  } yield dinner

Under the hood Scala uses the monadic property of Option by translating the for-comprehension like this:

def makeDinner(service: GrillService): Option[Dinner] =  
  service.getCharcoal.flatMap(charcoal =>
    service.getLighter.flatMap(lighter =>
      service.lightFire(charcoal, lighter).flatMap(fire =>
        service.getCornCob.flatMap(cornCob =>
          service.grill(fire, cornCob).map(dinner => dinner)))))

Java does not have type constructors, i.e. we can't define a flatMap() method throughout the type hierarchy (stripped down example):

interface Monad<T> {  
    <U> Monad<U> flatMap(Function<T, Monad<U>> f);
}

interface Option<T> extends Monad<T> {  
    @Override                      //     v---- does not compile
    <U> Option<U> flatMap(Function<T, Option<U>> f);
}

*) Javaslang has a Monad abstraction but it is not part of the Javaslang core.

If this were possible we could define a For() method that takes an arbitrary Monad instance. Under the hood we would call flatMap() and map() like Scala and our little world would be in order. But it isn't.

Because of missing type constructors we have to flatten the logic in the way that we define additional For() methods for every monadic type (like Option, Try, Either, Future, List, Stream, ...). We will add them to the next version of Javaslang. If we now fake a little bit by using a different formatting, the Grill example will look like this:

public static Option<Dinner> makeDinner(GrillService service) {  
    return
        For(service.getCharcoal(),
            service.getLighter(), (charcoal, lighter) -> For(
            service.lightFire(charcoal, lighter),
            service.getCornCob(), (fire, cornCob) -> For(
            service.grill(fire, cornCob)
        ).yield()));
}

🙈

Not really, right? We can't solve problems with formatting. The Java language has to change. We need syntactic sugar for monadic types. Will it ever come? I would say no. One reason is that Java's Optional does not obey the monad laws. Marcello La Rocca wrote a great article about it, see "How Optional Breaks the Monad Laws and Why It Matters".

However, the use of Javaslang's for-comprehension still makes sense for the reasons we already mentioned. Java does not support us on the language level. But what about the overhead? Is it still performant compared to plain if-statements?

Pap Lőrinc did a set of benchmarks using JMH that help us comparing the different implementations. (Thank you!!)

The original Grill example is implemented in two flavors (simplified here):

If/Else

if (fireOption.isDefined() && ...) {  
    return service.grill(fireOption.get(), ...);
}

For()/iterable

For(service.lightFire(...), fire ->  
    For(service.grill(fire, ...)).yield()
)
.toOption()

In a response to the previous blog post @heldev proposed to implement the example using Java's Stream:

j.u.s.Stream

Stream.of(charcoal, lighter, cornCob)  
      .allMatch(Optional::isPresent)
      ? service.lightFire(charcoal.get(), lighter.get())
               .flatMap(fire -> service.grill(fire, cornCob.get()))
      : Optional.empty();

Additionally we plan to integrate new flavors of For() into Javaslang (as described above) that embrace the monadic properties of our Value types:

For()/monadic

For(service.lightFire(...), fire -> For(  
    service.grill(fire, ...)
).yield())

Lőrinc applied both Option and Optional to the different implementations of the Grill example. However, Optional is not applicable to our for-comprehension because it does not implement Iterable.

These are the results:

for-benchmark

**) We omitted another benchmark here that measures the effect of using try/catch in conjunction with Option.get() instead of checking if the Option is defined.

Without going too much into detail we see that

  • the classical if/else approach is the fastest
  • followed by the new For()/monadic approach, which is still 57% as fast
  • followed by the j.u.s.Stream approach, which is 39% as fast as the fastest solution

The difference between if/else Optional and if/else Option first catched my eye. It is not clear why Option is slower. Both implementations isDefined() and get() are trivial. It might have to do with runtime optimizations by the JVM. In the presence of multiple implementations (Option has Some and None) the JVM probably can't optimize as well as in the case of flat types. We are still investigating it. (Any hints on this topic are welcome!)

It is no surprise that the For()/iterable implementation is 10x slower than the if/else solution. It is a general purpose method that is applicable to all kinds of Iterable types. It creates additional Iterator instances.

On the other hand For()/monadic is surprisingly fast, even faster than Java's Stream. It uses flatMap() and map() under the hood. These create new Option instances because our values are immutable/persistent.

Please note that these measures say nothing about the overall performance of an application. Beware of premature optimization. Instead I recommend to first write concise and readable code, then measure and identify bottlenecks, then optimize.

🍕

Bon appétit!

- Daniel