4

We are encountering problems when trying to use Scala code from Java. We are looking for a way to auto-bridge the various Scala and java collections without explicitly having to explicitly convert in our our Java code. When there are very few places it is reasonable to do it, but if it is wide spread and constantly adding new scala classes it just becomes impractical.

For instance in our Scala code we have some classes like this (many of these):

case class Product(id: String, group: String, attributes : Map[String,String])

def fooWithProducts(products : Seq[Product]) = ...

// create a product
val p1 = Product("id1","group1", Map("attribute1" -> "value1","attribute2" -> "value2")

val p2 = Product("id2","group1", Map("attribute1" -> "value1","attribute2" -> "value2")

fooWithProducts(Seq(p1,p2))

when we want write similar code in Java it becomes very cumbersome and we need to add a lot of auxillary boiler plate all over the place:

// JAVA
// create a product
HashMap<String, String> attributes = new HashMap<String, String>();
attributes.put("my-attribute","my-value");

// create a scala collection
scala.collection.immutable.Map<String, String> scalaAttributes = Conversions.toScalaImmutableMap(attributes);

// create a product
Product p = Product("id","group",scalaAttributes)

// create a scala sequence of product
ArrayList<Product> dataFeedsProducts = new ArrayList<Product>();
        dataFeedsProducts.add(Product);
Seq<Product> seq = JavaConversions.asScalaBuffer(dataFeedsProducts).seq();

fooWithProducts(seq)

// SCALA helpers

public class Conversions {
    //https://stackoverflow.com/questions/11903167/convert-java-util-hashmap-to-scala-collection-immutable-map-in-java/11903737#11903737
    @SuppressWarnings("unchecked")
    /**
     * Transforms a java map to a scala immutable map
     */
    public static <K, V> scala.collection.immutable.Map<K, V> toScalaImmutableMap(java.util.Map<K, V> javaMap) {
        final java.util.List<scala.Tuple2<K, V>> list = new java.util.ArrayList(javaMap.size());
        for (final java.util.Map.Entry<K, V> entry : javaMap.entrySet()) {
            list.add(scala.Tuple2.apply(entry.getKey(), entry.getValue()));
        }
        final scala.collection.Seq<Tuple2<K, V>> seq = scala.collection.JavaConverters.asScalaBufferConverter(list).asScala().toSeq();
        return (scala.collection.immutable.Map<K, V>) scala.collection.immutable.Map$.MODULE$.apply(seq);
    }

    /**
     * transforms a scala map to the java counterpart
     */
    public static <K,V> java.util.Map<K,V> mapAsJavaMap(scala.collection.immutable.Map<K,V> m) {
        if (m == null) {
            return null;
        }
        return JavaConversions.mapAsJavaMap(m);
    }

    public static<A> Seq<A> asScalaSeq(java.util.List<A> l) {
        return JavaConversions.asScalaBuffer(l).toSeq();
    }
}

We are wondering if there is a way to annotate the Scala code or auto-generate equivalent Java counterpart interfaces for these Scala idioms.

Avba
  • 14,822
  • 20
  • 92
  • 192
  • Have you seen https://docs.scala-lang.org/overviews/collections/conversions-between-java-and-scala-collections.html ? – Luca T. Feb 05 '18 at 09:24
  • @LucaT. What you linked seems to be rather for java -> scala direction, which is much less painful because scala has implicit conversions. – Andrey Tyukin Feb 05 '18 at 10:36

1 Answers1

7

Most frameworks that are written in Scala but are supposed to be used from both Scala and Java have a separate Java API. This is the case for Akka, Play and Spark, for example.

This is done because there are simply too many Scala constructs that do not map naturally to anything available in Java. While it is not entirely impossible to instantiate Scala classes from Java, doing so directly would result in very non-idiomatic and cumbersome Java code. Therefore, instead of moving the burden of converting Java data structures to Scala data structures to the users of the API, the designers simply create a separate Java API, and solve the problem with all the conversions and factory methods once and for all.

That is, instead of

  • writing a library in Scala and providing only scala API
  • writing class C_1 in Java, using Scala API, contaminating code with conversions (painful-1)
  • ...
  • writing class C_n in Java, using Scala API, contaminating code with conversions (painful-n)

you do the following:

  • write a library in Scala, provide Scala API first
  • add a foobar.api.java package to your library, implement a separate Java API, still using Scala (pain-singleton)
  • write class C_1 in Java using Java API (nice-and-pleasant-1)
  • ...
  • write class C_n in Java using Java API (nice-and-pleasant-n)

this should greatly reduce the overall amount of pain that you are inflicting on the users of the scala library (in your case: on yourself).

For the tiny code snippet that you have provided, this would mean, concretely:

  1. Provide factory methods so that Products can be instantiated from Java conveniently:

    // somewhere in api.java def product( id: String, grp: String, attrToVal: java.util.HashMap[String, String] ): Product = /* use JavaConversions, invoke constructor */

  2. Provide a version of fooWithProducts which can be called from Java without any conversions:

    // in api.java def fooWithProducts(ps: java.util.List[Product]): Foo = // Convert java List to scala Seq here, // call original fooWithProducts

Do this for every class and method which is supposed to appear in API. For this to work properly, the API must have a minimal surface area, and be focused and relatively simple when compared to the internals of the library.

Andrey Tyukin
  • 43,673
  • 4
  • 57
  • 93