Using the official RethinkDB Java driver in other JVM languages

When we released our official Java client driver earlier this month, we highlighted the opportunity that it presents for developers who want to use RethinkDB in other popular languages for the Java virtual machine.

In some JVM languages, our new client driver works right out of the box. In other languages, there are some roadblocks that will require third-party shims and other integration efforts. In this blog post, we will look at how you can use the driver today in a selection of popular JVM languages, including Scala, Clojure, Groovy, and Kotlin.

Interoperability with Java 8 lambdas

Most languages for the JVM are designed with some degree of Java interoperability, but not all of them have full support for the latest features in Java 8. The RethinkDB Java driver relies on the language’s newly-added support for lambda expressions, which provide a concise way for developers to pass anonymous functions to ReQL commands like map, reduce, and filter.

Practically every language for the JVM already had its own take on lambdas long before the feature arrived in Java 8. They all implement the feature in subtly different ways, which poses a number of compatibility problems when developers try to pass language-native anonymous functions over to Java code that expects to receive Java 8 lambas. That’s one of the biggest sticking points that users can expect to encounter right now using the RethinkDB Java driver in certain JVM languages, particularly Scala and Clojure.

Under the hood, a Java 8 lambda compiles down to an anonymous inner class with an apply method. In our Java client driver, we have a set of five ReqlFunction interfaces that describe anonymous functions with different arities. Each one represents an anonymous function with a specific number of parameters, between zero and four. An anonymous function with one parameter, for example, uses the ReqlFunction1 interface. In the client driver source code, any method that accepts an anonymous function uses one of those interfaces as the parameter type.

When you use the RethinkDB Java driver in languages that don’t interoperate with Java 8, you have to manually create an anonymous class that conforms with one of the ReqlFunction interfaces instead of using the built-in anonymous functions supported by the language.

Groovy

Groovy is a dynamic programming language with optional typing and rich support for creating embedded domain-specific languages. It’s also good for scripting and interactive execution via REPL. It has good interoperability with Java, which means that it works out of the box with our native Java driver. The following example demonstrates how to import the module, establish a connection to the database, perform a query, and display the output:

import com.rethinkdb.RethinkDB

r = RethinkDB.r
conn = r.connection().connect()
println(r.range(10).coerceTo("array").run(conn))

In Groovy, you can define an anonymous function by enclosing an expression in curly braces. If your function only takes one parameter, you can simply use the variable it as the implicit argument name instead of manually defining a function signature. Groovy’s anonymous functions are compatible with Java 8 lambdas, so you can pass them to ReQL commands like map and filter:

r.range(10).filter({it.mod(2).gt(0)}).map({it.mul(2)}).sum().run(conn)

Many of the periods and parentheses in Groovy are optional: you can leave them out and treat your ReQL expressions like an embedded domain-specific language. The following code is equivalent to the previous example:

r.range 10 filter {it.mod 2 gt 0} map {it.mul 2} sum() run conn

Groovy also supports operator overload, which you can use to make some ReQL sub-expressions look more native. You can even reopen existing classes and add operator overload methods, so we can overlay this behavior on top of the existing Java client driver. For example, here’s how you modify the ReqlExpr class to overload multiplication so that the * operator will perform the mul method:

import com.rethinkdb.RethinkDB
import com.rethinkdb.gen.ast.*

ReqlExpr.metaClass.multiply = {mul(it)}

...

r.range 10 filter {it.mod 2 gt 0} map {it * 2} sum() run conn

Unfortunately, Groovy’s equality checks and greater/less comparisons have special behaviors that require overloaded implementations to return boolean values. Since we can’t overload those operators with implementations that return ReQL expressions, we can’t easily make those operators behave as expected.

Groovy has built-in syntax for creating hash and array literals, which is often useful when you insert new items into the database. You can also use conventional property access to extract values from the JSON objects returned by the client driver:

r.table "fellowship" insert name: "Frodo", species: "hobbit" run conn

r.table "fellowship" filter {it.getField "species" eq "hobbit"} run conn each {
  println it.name
}

Let’s try something really crazy. Although you probably wouldn’t want to do this in production, you can overload property access on ReQL variables to make it behave like getField when you try to access a non-existent property:

import com.rethinkdb.RethinkDB
import com.rethinkdb.gen.ast.*

Var.metaClass.getProperty = {
  def meta = Var.metaClass.getMetaProperty it
  meta ? meta.getProperty(delegate) : getField(it)
}

...

r.table "fellowship" filter {it.species.eq "hobbit"} run conn each {
  println it.name
}

Kotlin

Kotlin is a relatively new statically-typed language developed by the folks at Jetbrains. It has basic type inference, excellent Java interoperability, and provides a nice balance between expressiveness and safety. Like Groovy, it also works entirely out of the box with the RethinkDB Java driver. The following example demonstrates how to import the module, establish a connection to the database, perform a query, and display the output:

import com.rethinkdb.RethinkDB.r

fun main(args: Array<String>) {
  val conn = r.connection().connect()
  println(r.range(10).coerceTo("array").run<List<Any>>(conn))
}

Kotlin also has built-in support for anonymous functions that are compatible with Java 8 lambdas. Although Kotlin isn’t as permissive as Groovy when it comes to optional punctuation, you can omit the parentheses when you invoke a method that takes an anonymous function as its sole argument:

println(r.range(10).filter {it.mod(2).gt(0)}.map {it.mul(2)}.sum().run<Long>(conn))

Kotlin supports operator overload on existing classes, but it has the same limitations as Groovy with respect to comparison and equality checks. The language will, however, let you enable infix invocation for specific methods which you can use to clean up the ReQL commands that can’t be replaced with operators:

import com.rethinkdb.RethinkDB.r
import com.rethinkdb.gen.ast.*

operator fun ReqlExpr.times(exprA: Any) : Mul {
  return mul(exprA)
}

operator fun ReqlExpr.mod(exprA: Any) : Mod {
  return mod(exprA)
}

infix fun ReqlExpr.gt(exprA: Any) : Gt {
  return gt(exprA)
}

fun main(args: Array<String>) {
  val conn = r.connection().hostname("rethinkdb-stable").connect()
  println(r.range(10).filter {it % 2 gt 0}.map {it * 2}.sum().run<Long>(conn))
}

Scala

Scala is a powerful functional programming language with a sophisticated type system. The following example demonstrates how to import the module, establish a connection to the database, perform a query, and display the output:

import com.rethinkdb.RethinkDB.r

val conn = r.connection().hostname("rethinkdb-stable").connect()
println(r.range(10).coerceTo("array").run(conn))

Scala’s Java 8 interoperability is still under development, so you have to manually create an anonymous class in order to use the RethinkDB Java driver with Scala out of the box. The following example iterates over a sequence of 10 elements, multiplies each value by 2, and then adds up the results:

println(r.range(10).map(new ReqlFunction1 {
  override def apply(x: ReqlExpr) = x.mul(2:Integer)
}).sum().run(conn))

If you want to get an early look at the language’s experimental support for Java 8 compatibility, you can run Scala with the -Xexperimental flag at the command line. With the flag enabled, you will be able to pass native Scala functions to methods that expect to receive Java 8 lambdas:

r.range(10).map(x => x.mul(2:Integer)).sum().run(conn)

Like Groovy, Scala also lets you omit many of the periods and parentheses. It also has a shorthand for writing anonymous functions–you can use an underscore to represent the parameter. The following code is a shorthand version of the previous example, with optional punctuation left out:

r range 10 map {_ mul(2:Integer)} sum() run conn

In some cases, you will still need to help Scala’s type inference figure out how to handle an anonymous function. For example, when you use the ReQL filter command, you have to tell the Scala compiler that the anonymous function you are passing is a ReqlFunction1:

r range 10 filter({_ mod 2 gt 0}:ReqlFunction1) coerceTo "array" run conn

Scala lets you define an implicit class to overload operators, providing a more native way to express some ReQL operations:

import com.rethinkdb.RethinkDB.r
import com.rethinkdb.gen.ast.ReqlExpr

implicit class RichReqlExpr(original: ReqlExpr) {
  def *(x: Integer) = original mul x
  def %(x: Integer) = original mod x
  def >(x: Integer) = original gt x
}

..

r range 10 filter({_ % 2 > 0}:ReqlFunction1) map {_ * 2} coerceTo "array" run conn

In the example above, I only define versions of the method that take an Integer. You’d likely also want to define versions that take AnyRef or ReqlExpr so that you can use those overloaded methods with other types besides literal number values.

Java 8 interoperability is an important part of the Scala 2.12 roadmap. If the features that are currently hidden behind the -Xexperimental flag work out of the box in version 2.12, it will go a long way towards making the RethinkDB Java driver a viable choice for Scala users.

Though I’m relatively inexperienced with Scala myself, I imagine that more seasoned Scala developers will be able to use implicits to smooth out the remaining rough edges.

Clojure

Clojure is a functional programming language that is modeled after Lisp. It is extremely flexible and expressive, with support for dynamic typing and macros. The following example demonstrates how to import the module, establish a connection to the database, perform a query, and display the output:

(defn -main [& args]
  (import com.rethinkdb.RethinkDB)
  (def r com.rethinkdb.RethinkDB/r)
  (def conn (-> (.connection r) .connect))
  (-> (.range r 10) (.coerceTo "array") (.run conn) println))

As a Lisp dialect, Clojure code is written with parenthetical s-expressions and prefix notation. You can invoke conventional Java methods by putting a period at the beginning of the method name. To express a chained series of method invocations in order instead of with nesting, you can use Clojure’s thread-first macro (->), as demonstrated in the previous example.

Unfortunately, Clojure doesn’t yet offer complete interoperability with the new features of Java 8. Several ReQL commands that rely on Java’s newly-added support for variadic arguments, for example, don’t work out of the box in Clojure. You have to manually wrap your parameters in an object-array, like this:

(-> (.expr r 5) (.add (object-array [5])) (.run conn) println)

When you define a function in Clojure, the underlying Java representation is an IFn instance, which you can’t pass to methods that expect to receive Java 8 lambdas. Like Scala without the experimental flag, Clojure requires you to manually create an anonymous inner class that conforms with the Java client driver’s various ReqlFunction interfaces. You can use Clojure’s reify function to create the anonymous inner class:

(-> (.range r 10)
    (.map (reify ReqlFunction1
          (apply [this x] (.mul x (object-array [2])))))
     .sum (.run conn) println)

It’s likely that the situation will improve in the future when Clojure develops better Java 8 interoperability. It’s also likely possible for a skilled Clojure developer to create shims and macros that ameliorate the compatibility issues. For now, users are probably better off trying clj-rethinkdb, a third-party RethinkDB driver written natively in Clojure.

What about JRuby?

JRuby is an implementation of the Ruby programming language that runs on the JVM. It’s a popular choice for developers who want the expressiveness and dynamic execution of Ruby while retaining full access to the Java library ecosystem. A lot of existing Ruby code that doesn’t rely on native extensions will work out of the box on JRuby. As such, we recommend using the official RethinkDB Ruby client driver in the JRuby environment.

Next steps

Want to try RethinkDB in your own JVM project? Check out our ten-minute guide to RethinkDB and refer to our Java client driver installation instructions for details about how to use the driver in Maven, Gradle, and Ant.