Fusing methods for productivity

Donald Raab
Javarevisited
Published in
4 min readJul 3, 2020

When an iteration pattern is so common you give it a name.

Welcome to SkyBay, where the bay is fused with the sky using docks as zippers

When anySatisfy doesn’t satisfy

Sometimes you want to look to see if a collection contains an element that matches a value based on one of it’s attributes. In the example below, I use the method anySatisfy from Eclipse Collections to see if any of the pairs have a “2” or “3” in the getTwo attribute.

@Test
public void anySatisfyContains()
{
MutableList<Pair<Integer, String>> list =
Lists.mutable.with(
Tuples.pair(1, "1"),
Tuples.pair(2, "2"));

Assert.assertTrue(
list.anySatisfy(pair -> pair.getTwo().equals("2")));
Assert.assertFalse(
list.anySatisfy(pair -> pair.getTwo().equals("3")));
}

We could extract the value for pair.getTwo() using the collect method with a Function specified as a method reference.

@Test
public void eagerCollectAnySatisfy()
{
MutableList<Pair<Integer, String>> list =
Lists.mutable.with(
Tuples.pair(1, "1"),
Tuples.pair(2, "2"));

Assert.assertTrue(
list.collect(Pair::getTwo)
.anySatisfy(each -> each.equals("2")));
Assert.assertFalse(
list.collect(Pair::getTwo)
.anySatisfy(each -> each.equals("3")));
}

Unfortunately, this is even more code, and also creates a temporary List after the call to collect. The method anySatisfy when used with an equality test, can be simplified to contains. I’ll use contains instead of anySatisfy.

@Test
public void eagerCollectContains()
{
MutableList<Pair<Integer, String>> list =
Lists.mutable.with(
Tuples.pair(1, "1"),
Tuples.pair(2, "2"));

Assert.assertTrue(
list.collect(Pair::getTwo).contains("2"));
Assert.assertFalse(
list.collect(Pair::getTwo).contains("3"));
}

Now the code is more concise and readable, but we still have the extra temporary List creation to remove. This can be fixed by adding a call to asLazy.

@Test
public void lazyCollectContains()
{
MutableList<Pair<Integer, String>> list =
Lists.mutable.with(
Tuples.pair(1, "1"),
Tuples.pair(2, "2"));

Assert.assertTrue(
list.asLazy()
.collect(Pair::getTwo)
.contains("2"));
Assert.assertFalse(
list.asLazy()
.collect(Pair::getTwo)
.contains("3"));
}

Now we have a readable solution that doesn’t create too much garbage but has three method calls to see if a collection contains an element that has an attribute that equals a specific value.

We can do better.

Fusing Methods

Let’s add a default method to RichIterable that can provide this functionality.

But first, let’s write a test.

@Test
public void collectContains()
{
MutableList<Pair<Integer, String>> list =
Lists.mutable.with(
Tuples.pair(1, "1"),
Tuples.pair(2, "2"),
Tuples.pair(3, null));

Assert.assertTrue(
list.collectContains(Pair::getTwo, "2"));
Assert.assertFalse(
list.collectContains(Pair::getTwo, "3"));
Assert.assertTrue(
list.collectContains(Pair::getTwo, null));
Assert.assertFalse(
list.collectContains(Pair::getOne, null));
}

We want to fuse the collect and contains methods, so I will start out calling it collectContains. I added some tests as well to make sure that we can compare to null.

The method collectConains on RichIterable will be eager, because it returns a boolean. I will implement the method using anySatisfy, which is also an eager method that returns a boolean.

default <V> boolean collectContains(
Function<? super T, ? extends V> function,
V value)
{
if (null == value)
{
return this.anySatisfy(
each -> null == function.valueOf(each));
}
return this.anySatisfy(
each -> value.equals(function.valueOf(each)));
}

This is my initial attempt at the implementation supporting null values. The tests pass. Win!

Let me see what it looks like using a ternary operator.

default <V> boolean collectContains(
Function<? super T, ? extends V> function,
V value)
{
return this.anySatisfy(null == value ?
each -> null == function.valueOf(each) :
each -> value.equals(function.valueOf(each)));
}

Tests pass. Win!

I like the ternary operator approach better but will make it slightly easier to parse in the next iteration by extracting the Predicate into a variable.

Update: Another Eclipse Collections contributor (Vladimir Zakharov) read the initial version of the blog and suggested there might be a better name for the collectContains method, based on a similar pattern we have used before for fused methods that take a Function. If we add By as a suffix to the method and drop the collect prefix we will get containsBy.

Update July 23, 2020: I came up with a simpler solution today and updated below.

default <V> boolean containsBy(
Function<? super T, ? extends V> function,
V value)
{
Objects.requireNonNull(function);
return this.anySatisfy(
each -> Objects.equals(value, function.valueOf(each)));
}

This would result in the code in the test looking as follows.

@Test
public void containsBy()
{
MutableList<Pair<Integer, String>> list =
Lists.mutable.with(
Tuples.pair(1, "1"),
Tuples.pair(2, "2"),
Tuples.pair(3, null));

Assert.assertTrue(
list.containsBy(Pair::getTwo, "2"));
Assert.assertFalse(
list.containsBy(Pair::getTwo, "3"));
Assert.assertTrue(
list.containsBy(Pair::getTwo, null));
Assert.assertFalse(
list.containsBy(Pair::getOne, null));
}

I think this is much better. Thank you for the suggestion Vladimir Zakharov!

Examples of other methods that have aBy suffix and take a Function as a parameter are aggregateBy, groupBy, countBy, minBy, maxBy, etc. I wrote a blog a couple years ago talking about the productivity gains provided by the preposition By.

So should we add this method?

I think we should add containsBy to Eclipse Collections RichIterable type. This is a common enough pattern that the benefits will significantly outweigh the implementation and support costs. There isn’t much more that needs to be done beyond adding some additional tests so I will probably just submit a pull request soon.

Can we add this to Java Stream?

The answer is both “of course” and “it depends”. I don’t think we can call contains on a Stream today as it doesn’t exist. Take a look for yourself. You can of course implement the same code using anyMatch. The big question is would this method be considered of high enough value to add to the JDK. The method name could be mapContains or containsBy and could be as simple as adding this default method to Stream.

default <V> boolean containsBy(
Function<? super T, ? extends V> function,
V value)
{
Objects.requireNonNull(function);
return this.anyMatch(
each -> Objects.equals(value, function.apply(each)));
}

This might be a more valuable method to add to Java Stream than just contains by itself.

What do you think?

I am a Project Lead and Committer for the Eclipse Collections OSS project at the Eclipse Foundation. Eclipse Collections is open for contributions. If you like the library, you can let us know by starring it on GitHub.

--

--

Donald Raab
Javarevisited

Java Champion. Creator of the Eclipse Collections OSS Java library (https://github.com/eclipse/eclipse-collections). Inspired by Smalltalk. Opinions are my own.