What I miss in Java after working with Kotlin / Scala

What I miss in Java after working with Kotlin / Scala


Recently, I often hear that Java has become an obsolete language on which it is difficult to build large supported applications. In general, I do not agree with this view. In my opinion, the language is still suitable for writing fast and well-organized applications. However, I confess, it also happens that when you write code every day, you sometimes think: “how good it would be to decide this thing from another language”. In this article I wanted to share my pain and experience. We look at some Java problems and how they could be resolved in Kotlin/Scala. If you have a similar feeling or you are just wondering what other languages ​​can offer, I ask for a cat.



Extending Existing Classes


Sometimes it happens that it is necessary to expand an existing class without changing its internal contents. That is, after the creation of the class, we supplement it with other classes. Consider a small example. Suppose we have a class that represents a point in two-dimensional space. In different places of our code, we need to serialize it in Json and in XML.

Let's see how it might look in Java using the Visitor pattern.
  public class DotDemo {

  public static class Dot {
  private final int x;
  private final int y;

  public Dot (int x, int y) {
  this.x = x;
  this.y = y;
  }

  public String accept (Visitor visitor) {
  return visitor.visit (this);
  }

  public int getX () {return x;  }
  public int getY () {return y;  }
  }

  public interface Visitor {
  String visit (Dot dot);
  }

  public static class JsonVisitor implements Visitor {
  @Override
  public String visit (Dot dot) {
  return string
  .format ("" +
  "{" +
  "\" x \ "=% d," +
  "\" y \ "=% d" +
  "}",
  dot.getX (), dot.getY ());
  }
  }

  public static class XMLVisitor implements Visitor {
  @Override
  public String visit (Dot dot) {
  return "& lt; dot & gt;"  + "\ n" +
  "& lt; x & gt;"  + dot.getX () + "& lt;/x & gt;"  + "\ n" +
  "& lt; y & gt;"  + dot.getY () + "& lt;/y & gt;"  + "\ n" +
  "& lt;/dot & gt;";
  }
  }

  public static void main (String [] args) {
  Dot dot = new Dot (1, 2);

  System.out.println ("-------- JSON -----------");
  System.out.println (dot.accept (new JsonVisitor ()));

  System.out.println ("-------- XML ​​------------");
  System.out.println (dot.accept (new XMLVisitor ()));
  }
 }
  

Learn more about the pattern and its use

It looks quite volume, is not it? Is it possible to solve this problem more elegantly with the help of language aids? Scala and Kotlin nod positively. This is achieved using the method extension mechanism. Let's see what it looks like.

Extensions in Kotlin
  data class Dot (val x: Int  , val y: Int)
//implicitly get object reference
 fun Dot.convertToJson (): String =
  "{\" x \ "= $ x, \" y \ "= $ y}"

 fun Dot.convertToXml (): String =
  "" "& lt; dot & gt;
  & lt; x & gt; $ x & lt;/x & gt;
  & lt; y & gt; $ y & lt;/y & gt;
  & lt;/dot & gt; "" "


 fun main () {
  val dot = Dot (1, 2)
  println ("-------- JSON -----------")
  println (dot.convertToJson ())
  println ("-------- XML ​​-----------")
  println (dot.convertToXml ())
 }  


Extensions in Scala
  object DotDemo extends App {

//val is default
  case class Dot (x: Int, y: Int)

  implicit class DotConverters (dot: Dot) {
  def convertToJson (): String =
  s "" "{" x "= $ {dot.x}," y "= $ {dot.y}}" ""
  def convertToXml (): String =
  s "" "& lt; dot & gt;
  & lt; x & gt; $ {dot.x} & lt;/x & gt;
  & lt; y & gt; $ {dot.y} & lt;/y & gt;
  & lt;/dot & gt; "" "
  }

  val dot = Dot (1, 2)
  println ("-------- JSON -----------")
  println (dot.convertToJson ())
  println ("-------- XML ​​-----------")
  println (dot.convertToXml ())
 }
  


It looks much better. Sometimes this is very lacking with heavy mappings and other transformations.

Multi-threaded computing chain


Now everyone is talking about asynchronous computing and blocking bans in the execution threads. Let's imagine this problem: we have several sources of numbers, where the first just gives a number, the second returns the answer after the first one. As a result, we must return a string with two numbers.

It can be schematically represented as follows


Let's try to solve the problem in Java first

Java Example
  private static CompletableFuture & lt; Optional & lt; String & gt;  & gt;  calcResultOfTwoServices (
  Supplier & lt; Optional & lt; Integer & gt; & gt;  getResultFromFirstService,
  Function & lt; Integer, Optional & lt; Integer & gt; & gt;  getResultFromSecondService
  ) {
  return CompletableFuture
  .supplyAsync (getResultFromFirstService)
  .thenApplyAsync (firstResultOptional - & gt;
  firstResultOptional.flatMap (first - & gt;
  getResultFromSecondService.apply (first) .map (second - & gt;
  first + "" + second
  )
  )
  );
  }  


In this example, our number is wrapped in Optional to control the result. In addition, all actions are performed inside the CompletableFuture for convenient work with threads. The main action takes place in the thenApplyAsync method. In this method, we get Optional as an argument. Next, flatMap is called to control the context. If the received Optional is returned as Optional.empty, then we will not go to the second service.

Total, what we got? With the help of CompletableFuture and the Optional c flatMap and map capabilities, we were able to solve the problem. Although, in my opinion, the solution does not look very elegant: before you understand what's the matter, you must read the code. And what would be the case with two or more data sources?

Could we somehow help solve the problem of language. And again we turn to Scala. Here’s how to do this with Scala tools.

Scala Example
  def calcResultOfTwoServices (getResultFromFirstService: Unit = & gt  ; Option [Int],
  getResultFromSecondService: Int = & gt;  Option [Int]) =
  Future {
  getResultFromFirstService ()
  } .flatMap {firsResultOption = & gt;
  Future {firsResultOption.flatMap (first = & gt;
  getResultFromSecondService (first) .map (second = & gt;
  s "$ first $ second"
  )
  )}
  }  


Looks familiar. And this is not by chance. It uses the scala.concurrent library, which is mainly a wrapper over java.concurrent. Well, how else can Scala help us? The fact is that chains of the form flatMap, ..., map can be represented as a sequence in for.

Second version Scala example
  def calcResultOfTwoServices (getResultFromFirstService: Unit  => Option [Int],
  getResultFromSecondService: Int = & gt;  Option [Int]) =
  Future {
  getResultFromFirstService ()
  } .flatMap {firstResultOption = & gt;
  Future {
  for {
  first & lt; - firstResultOption
  second & lt; - getResultFromSecondService (first)
  } yield s "$ first $ second"
  }
  }  


It's better, but let's try to change our code again. Let's connect the cats library.

The third version of the Scala example
  import cats.instances.future.  _

  def calcResultOfTwoServices (getResultFromFirstService: Unit = & gt; Option [Int],
  getResultFromSecondService: Int = & gt;  Option [Int]): Future [Option [String]] =
  (for {
  first & lt; - OptionT (Future {getResultFromFirstService ()})
  second & lt; - OptionT (Future {getResultFromSecondService (first)})
  } yield s "$ first $ second"). value  


Now it’s not so important what OptionT means. I just want to show how simple and short this operation can be.

What about Kotlin? Let's try to do something like that on quiche.

Example on Kotlin
  val result = async {
  withContext (Dispatchers.Default) {getResultFromFirstService ()} ?. let {first - & gt;
  withContext (Dispatchers.Default) {getResultFromSecondService (first)}?. let {second - & gt;
  "$ first $ second"
  }
  }
  }  


This code has its own features. First, it uses the Kotlin Corutin mechanism. Tasks inside async are performed in a special thread pool (not ForkJoin) with the work stealing mechanism. Secondly, this code requires a special context, from which keywords like async and withContext are taken.

If you like Scala Future, but you write on Kotlin, you can pay attention to similar Scala wrappers. Like this.

Working with streams


To show the problem in more detail, let's try to extend the last example: let's turn to the most popular Java programming tools - Reactor , on Scala - fs2 .

Consider the line-by-line reading of 3 files in the stream and try to find matches there.
Here is the easiest way to do this with Reactor in Java.

Example from Reactor to Java
  private static Flux & lt; String & gt;  glueFiles (String filename1, String filename2, String filename3) {
  return getLinesOfFile (filename1) .flatMap (lineFromFirstFile - & gt;
  getLinesOfFile (filename2)
  .filter (line - & gt; line.equals (lineFromFirstFile))
  .flatMap (lineFromSecondFile - & gt;
  getLinesOfFile (filename3)
  .filter (line - & gt; line.equals (lineFromSecondFile))
  .map (lineFromThirdFile - & gt;
  lineFromThirdFile
  )
  )
  );
  }  


Not the most optimal way, but indicative. It is not difficult to guess that with more logic and appeals to third-party resources, the complexity of the code will increase. Let's see an alternative to the syntax sugar for-comprehension.

Example with fs2 on Scala
  "java"> def findUniqueLines (filename1: String  , filename2: String, filename3: String): Stream [IO, String] =
  for {
  lineFromFirstFile & lt; - readFile (filename1)
  lineFromSecondFile & lt; - readFile (filename2) .filter (_. equals (lineFromFirstFile))
  result & lt; - readFile (filename3) .filter (_. equals (lineFromSecondFile))
  } yield result  


It seems not so much change, but it looks much better.

Separate business logic with higherKind and implicit


Let's go ahead and see how we can further improve our code. I want to warn you that the next part may not be immediately clear. I want to show the possibilities, and the implementation method is left behind. A detailed explanation requires at least a separate article. If there is a desire/comments - I will follow in the comments to answer the questions and write the second part with a more detailed description :)

So, imagine a world in which we can set business logic regardless of the technical effects that may arise during development.For example, we can make so that each following request to a DBMS or a third-party service is executed in a separate thread. In unit tests, we need to make a stupid moment in which nothing happens. And so on.

Perhaps some thought about the BPM engine, but today is not about him. It turns out that this problem can be solved with the help of some functional programming patterns and language support. In one place we can describe the logic something like this.

In one place we can describe the logic something like this
  def  makeCatHappy [F [_]: Monad: CatClinicClient] (): F [Unit] =
  for {
  catId & lt; - CatClinicClient [F] .getHungryCat
  memberId & lt; - CatClinicClient [F] .getFreeMember
  _ & lt; - CatClinicClient [F] .feedCatByFreeMember (catId, memberId)
  } yield ()  


Here, F [_] (read as “ef with a hole”) means a type above a type (sometimes in Russian-language literature it is called a type). It can be List, Set, Option, Future, etc. Anything that is a different type of container.

Then just change the context of the code execution. For example, for prod environment we can do something like this.

What a combat code might look like
  class RealCatClinicClient extends CatClinicClient [Future  ] {
  override def getHungryCat: Future [Int] = Future {
  Thread.sleep (1000)//doing some calls to db (waiting 1 second)
  40
  }
  override def getFreeMember: Future [Int] = Future {
  Thread.sleep (1000)//doing some calls to db (waiting 1 second)
  2
  }
  override def feedCatByFreeMember (catId: Int, memberId: Int): Future [Unit] = Future {
  Thread.sleep (1000)//happy cat (waiting 1 second)
  println ("so testy!")//Don't do like that.  It is just for debug
  }
 }  


What a test code might look like
  class MockCatClinicClient extends CatClinicClient [Id  ] {
  override def getHungryCat: Id [Int] = 40
  override def getFreeMember: Id [Int] = 2
  override def feedCatByFreeMember (catId: Int, memberId: Int): Id [Unit] = {
  println ("so testy!")//Don't do like that.  It is just for debug
  }
 }  


Our business logic now does not depend on what frameworks, http-clients and servers we used. At any time we can change the context, and the tool will change.

This is achieved by features such as higherKind and implicit. Consider the first, and for this we return to Java.

Let's look at the code
  public class Calcer {
  private CompletableFuture & lt; Integer & gt;  getCalc (int x, int y) {
  }
 }
  


How many ways are there to return the result? Enough. We can deduct, add, swap and more. Now imagine that we are given clear requirements. We need to add the first number to the second. How many ways can we do this? if you try hard and do a lot of work ... there’s only one.

Here it is
  public class Calcer {
  private CompletableFuture & lt; Integer & gt;  getCalc (int x, int y) {
  return CompletableFuture.supplyAsync (() - & gt; x + y);
  }
 }  


But what if the call to this method is hidden, and we want to test in a single-threaded environment? Or what if we want to change the class implementation by removing/replacing the CompletableFuture. Unfortunately, in Java we are powerless and have to change the API of the method. Take a look at the alternative to Scala.

Consider the trait
  trait Calcer [F [_]] {
  def getCulc (x: Int, y: Int): F [Int]
 }
  


Create trawls (the closest analog is an interface in Java) without specifying the container type of our integer value.

Then we can simply create different implementations as necessary.

For example,
  val futureCalcer: Calcer [Future] = (  x, y) = & gt;  Future {x + y}
  val optionCalcer: Calcer [Option] = (x, y) = & gt;  Option (x + y)
  


In addition, there is such an interesting thing as Implicit. It allows you to create the context of our environment and implicitly select the implementation of the treit based on it.

For example,
  def userCalcer [F [_]] (  implicit calcer: Calcer [F]): F [Int] = calcer.getCulc (1, 2)

  def doItInFutureContext (): Unit = {
  implicit val futureCalcer: Calcer [Future] = (x, y) = & gt;  Future {x + y}
  println (userCalcer)
  }
  doItInFutureContext ()

  def doItInOptionContext (): Unit = {
  implicit val optionCalcer: Calcer [Option] = (x, y) = & gt;  Option (x + y)
  println (userCalcer)
  }
  doItInOptionContext ()  


Simply implicit before val - adding a variable to the current environment, and implicit as a function argument means taking a variable from the environment. This is a bit like an implicit closure.

Taken together, it turns out that we can create a combat and test environment quite concisely without using third-party libraries.
But what about kotlin
In fact, in a similar way, we can do in kotlin:
  interface Calculator & lt; T & gt;  {
  fun eval (x: int, y: int): t
 }

 object FutureCalculator: Calculator & lt; CompletableFuture & lt; Int & gt; & gt;  {
  override fun eval (x: int, y: int) = CompletableFuture.supplyAsync {x + y}
 }

 object OptionalCalculator: Calculator & lt; Optional & lt; Int & gt; & gt;  {
  override fun eval (x: int, y: int) = Optional.of (x + y)
 }

 fun & lt; T & gt;  Calculator & lt; T & gt; .useCalculator (y: Int) = eval (1, y)

 fun main () {
  with (FutureCalculator) {
  println (useCalculator (2))
  }
  with (OptionalCalculator) {
  println (useCalculator (2))
  }
 }  

Here we also set the execution context of our code, but unlike Scala we clearly mark this.
Thanks to Beholder for an example.


Output


In general, this is not all my pain. There are more. I think that each developer has accumulated their own. For myself, I realized that the main thing is to understand what is really necessary for the benefit of the project. For example, in my opinion, if we have a rest service that acts as a kind of adapter with a bunch of mapping and simple logic, then all the functionality above is not really useful. For such tasks, Spring Boot + Java/Kotlin is perfect. There are other cases with a large number of integrations and aggregation of some information. For such tasks, in my opinion, the last option looks very good. In general, it's great if you can choose a tool based on a task.

Useful resources:

  1. Link to all full versions of the examples above
  2. Learn more about Kortlin Kortlin
  3. A nice introductory book on functional programming on Scala

Source text: What I miss in Java after working with Kotlin / Scala