Auto-casting Java Functional Interfaces to BoxLang Functions

Ok, here’s the last one (for now). One of the features we’ve built into BoxLang in our pursuit of bringing Java and BL closer together is the ability of our function caster to take an instance of a functional interface from Java and wrap it up as a first-class BoxLang function.

Now, you may be saying, didn’t you already add that? We did already have the ability to define a function in BoxLang and pass it into a Java library for Java to use, but this is the other way around! This gives you the ability to take a function defined in Java and pass it into BoxLang to use.

Now, you may be thinking what’s the use case for this-- and perhaps you won’t have one. But one of our goals is not only to make Java easy to use from inside of BoxLang, but also to make BoxLang easy to use from Java! POV: you’re a Java developer looking to add some sweet, dynamic JVM goodness into your app much like Java devs may use some Kotlin or Groovy in the mix. This is why we wrote our JSR-223 wrapper (the only CF engine with 100% of the interfaces implemented!) before we even made the web runtime for BoxLang.

Ok, examples. If you’re not familiar with Java, they love classes. Like really love them! So much so that functions are actually an instance of a class that implements a “functional” interface and has a single method. Java’s Lambda ( foo ) -> bar syntax is just some sugar that creates an anonymous instance of a class implementing said interface. As such, all functional interfaces aren’t the same. Java is a strictly-typed static language, so functions must know at compile type exactly what sort of arguments and return values (if any) they’ll be dealing with.

Some common functional interfaces in Java are:

  • Predicate - Accepts a single arg and returns a boolean (useful for filtering)
  • Consumer - Accepts a single arg and returns nothing (useful for each operations)
  • Supplier - Accepts no args and returns a value (useful for the 3rd item on a list)
  • Function - Accepts a single arg and returns a value (useful for mapping)
  • Comparator - Accepts two args and returns an integer (useful for comparison/sorting)

And since Java doesn’t handle arguments dynamically like CF/BL, there’s a bunch of variations like

  • BiFunction - Accepts 2 args and returns a value (useful for reducing)
  • BiPredicate - Accepts 2 args and returns a boolean

This first example is just a simple proof of concept. The JDK class Predicate has an isEqual() method that returns a simple pre-built predicate that checks if an incoming value is equal to the value that the predicate was instantiated with. We can get this Java function and cast it directly to a BoxLang function, we just need to tell BoxLang the name of the Java functional interface it implements so it can be cast correctly. Let’s try it out:

// We can import Java classes directly in BoxLang!
import java.util.function.Predicate;

// Cast the Java predicate into a BoxLang function we can just call 
isBrad = Predicate.isEqual( "brad" ) castas "function:Predicate"

isBrad( "brad" ) // true
isBrad( "luis" )  // false
[ "brad", "luis", "jon" ].filter( isBrad )  // [ "brad" ]

Here’s another an example using only BoxLang code that uses a pre-built Java function that comes bundled with the JDK. The Collections class in the JDK has a bunch of pre-made functions for doing common things with Java Collections. The reverseOrder() method on this class will give you an instance of a Comparator function. The arraySort() BIF in Boxlang is already configured to “know” it can accept a BoxLang function OR it will auto-cast a Java functional interface of Comparator if you pass one.

import java.util.Collections;
// Use the Java Comparator directly as an array sort callback!
[ 1, 7, 3, 99, 0 ].sort( Collections.reverseOrder() ) // 99, 7, 3, 1, 0

And the last example involves the JSR-223 interface, where Java developers can call BoxLang code right from inside Java code. They can define a Java Lambda and pass it directly in like so:

// This is Java code...
Bindings bindings = JSREngine.createBindings();
// Java requires us to type the lambda 
bindings.put( "isEven", ( Predicate<Integer> ) ( n ) -> n % 2 == 0 );

Object result = JSREngine.eval( 
  """
    // This is BoxLang code using our Java lambda from above
    [1,2,3,4,5,6,7,8,9,10].filter( isEven )
  """, bindings );
// result now contains [ 2, 4, 6, 8, 10 ]

Hopefully these make sense. I know they require a bit more knowledge of Java, but learning a bit of Java is a great thing to do as a developer who works on the JVM :slight_smile: You can allow a Java functional interface (or any SAM interface) to be passed (and auto-cast) to any BoxLang function by putting the Java interface name in the function type just like we did above with the first castas example.

function performFilter( function:Predicate callback, any value ) {
  return callback( value );
}

That BoxLang function will accept another BoxLang function, OR a Java Predicate ,which it will auto cast for you. All of the core higher order BIFs in BoxLang have been updated to accept functional interfaces. Details here:
https://ortussolutions.atlassian.net/browse/BL-366

Ticket for original feature:
https://ortussolutions.atlassian.net/browse/BL-338

1 Like