New member methods for Java Streams

In BoxLang we already have member methods you’re used to on the common data types such as structs, arrays, strings, numbers, etc. Since we intercept method calls, we have the ability to define method methods on ANY variable type. We’ve added some nice little helpers to Java Streams to wrap up some of the cool stream Collectors we’ve built. A collector is a terminal operation on a stream which “collects” the values in the stream into a single object, kind of like a pre-built reduce operation but more powerful.

A quick note on Streams-- I’ve been posting a lot about them, and you may be wondering what the difference is between a stream and an array for example-- especially since a lot of my code creates a stream out of an array just for the sake of example. While streams have familiar methods like map(), foreach(), findFirst(), etc, it’s important to note that a stream is NOT the same as a data structure like an array or struct. While an array always has an exact number of items in it, a stream is nothing more than a pipe of information. It can stream data from a source of unknown size, or it can literally be infinite!

Consider the following example:

import java.util.stream.Stream;

// create stream of all even numbers
evenNumbers = Stream.iterate(0, n -> n + 2)

This stream will produce all even numbers to infinity. Try doing that with an array! :slight_smile: You obviously wouldn’t just want to slap a foreach() on that or it would loop forever. Instead, you would use a stream method like limit(), findFirst(), or takeWhile() to decide how much of the stream you wanted to consume.

Like this example that finds the first number in the infinite Fibonacci series that is greater than 1000. Note, this is all BoxLang code!

import java.util.stream.Stream;

Stream.iterate([0, 1], f -> [ f[2], f[1] + f[2] ])
  .map( f -> f[1] )
  .dropWhile( n -> n < 1000 )
  .findFirst()
  .get() // 1597

Streams are super powerful and they don’t process sequentially like myarray.filter().map().each() do. The operations can be executed in parallel with all stages happening at the same time. The JDK uses streams a lot, and we LOVE them inside of BoxLang.

So here’s the examples-- again I know these examples have simpler ways of doing some things using pure CFML/BL, but there are many other use cases for Streams that do things structs, arrays, and queries can’t do.

Collect a stream of objects into a BoxLang array with .toBXArray()

Similar to the built-in .toList() but instead of a java.util.List you get a first class Boxlang array.

import java.util.stream.IntStream;

// create a stream of all integers between 1 (inclusive) and 6 (exclusive)
result = IntStream.range(1, 6).toBXArray(); // [ 1, 2, 3, 4, 5 ]

Collect a stream of Map entries back into a Struct with .toBXStruct()

foods = {
  'apples'  : 'healthy',
  'bananas' : 'healthy',
  'pizza'   : 'junk',
  'tacos'   : 'junk'
};
// result contains only healthy food
result = foods.entrySet().stream()
  .filter( e -> e.getValue() == 'healthy' )
  .toBXStruct();

The toBXStruct() member method also accepts any type you can pass to structNew() like sorted or ordered.

Collect an array of structs into an existing query object with .toBXQuery()

// create empty query to receive data
qry = queryNew( "name,title", "varchar,varchar" );

[
  	{ name: "Brad", title: "Developer" },
  	{ name: "Luis", title: "CEO" },
  	{ name: "Jorge", title: "PM" }
].stream().toBXQuery( qry );

Collect a stream of objects into a delimited list with .toBXList()

domain = [ "www", "google", "com" ].stream().toBXList( "." )

These should give you some handy pre-built stream Collectors to help you get a stream of data back into a native BoxLang datatype.

Feature ticket (and more examples): [BL-388] - Welcome

I posted this in Slack, but figured this was a better place to put it.

I like the idea here I just wonder if instead of adding toBXStruct toBXArray etc, you could instead just use the existing castAs operator here. This is what I’m thinking:

domain = [ "www", "google", "com" ].stream().castAs( "array" ); // boxlang.runtime.types.Array

Could you even make it so castAs can accept any java type? For example

domain = [ "www", "google", "com" ].stream().castAs( "java.lang.ArrayList" ); // java.lang.ArrayList

Just thinking out loud

1 Like

Here my Slack reply posted here as well :slight_smile:

Interesting idea. The reason for the current direction was

  • It’s not really “casting” in any traditional sense of the term. It’s collecting a stream using some very specific java.util.stream.Collector implementations we’ve written
  • Streams already have built in collectors with a naming precedent of .toList() and .toArray() so it makes sense to move forward with .toDataType() but with some prefix to demarcate between Java lists and BoxLang lists and Java arrays and BoxLang arrays.
  • The new member methods, since they wrap stream collectors, are meant to only attach themselves to Stream objects.
  • .castAs() could be attached to all variables of all types like the methods in java.lang.Object but I’m not a fan of having global member methods like that. I feel an operation that can be applied to any variable should be a first-class operator in the language like castas or instanceof etc. But again, the feature described here is very closely tied to streams and isn’t casting.