Fusing methods for productivity
When an iteration pattern is so common you give it a name.
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.