Preposition Preference

What’s up? A preposition.

Photo taken at Grounds for Sculpture in Hamilton, NJ

A friend of mine at Rutgers University would always respond to the question “What’s up?” with the consistent witty response: “A preposition.” I fell into this trap far too many times.

Have you ever thought about how much we use prepositions in our Java APIs?

We use several different prepositions in APIs in Eclipse Collections. Each one conveys a different meaning. Some prepositions that appear in Eclipse Collections multiple times are “with, of, by, as, to, from, into”. When we use a preposition in an API, it should help convey meaning clearly. If it doesn’t, then we would have been better off without it.

Two prepositions enter. One preposition leaves.

At JavaOne this year, I described a battle we once had between two prepositions for naming our collection factory methods in the Eclipse Collections API. The battle was between of and with.

MutableList<String> list = Lists.mutable.of("1", "2", "3");
                           vs.
MutableList<String> list = Lists.mutable.with("1", "2", "3");

After an intense debate, we wound up with both options for our collection factory classes. We thought this was one place where we could provide multiple options to allow developers to use their own preference. This however was not the end of the story. Sometimes there is more than just a single battle to be won.

The prepositions of and with both work well for naming factory methods for creating collections. I personally prefer with, mostly because this is what was used with Smalltalk. In Smalltalk, I would regularly write the following:

|set|
set := Set with: ‘1’ with: ‘2’ with: ‘3’.

The following is the equivalent using Java with Eclipse Collections.

MutableSet<String> set = Sets.mutable.with("1", "2", "3");

If you prefer, you can also create a collection using the of factory method.

MutableSet<String> set = Sets.mutable.of("1", "2", "3");

There are also forms that take an Iterable as a parameter. These are called ofAll and withAll.

In java.util.Collection, there are methods for adding and removing elements to and from collections. They are named add, addAll, remove and removeAll. These four methods return boolean. This makes them unsuitable for writing code fluently.

We have our own Mutable interfaces in Eclipse Collections, so we knew we could fix the fluency problem by using a one of the two prepositions. We decided to go with with, because with has a natural opposite named without.

Set<String> set = 
Sets.mutable.with("1", "2", "3")
.with("4")
.without("2");
Assert.assertEquals(Sets.mutable.with("1", "3", "4"), set);

This naming pattern also worked well when adding elements via an Iterable.

Set<String> set =
Sets.mutable.with("1", "2", "3")
.withAll(Lists.mutable.with("4"))
.withoutAll(Lists.mutable.with("1", "3"));
Assert.assertEquals(Sets.mutable.with("2", "4"), set);

As you can see, we have with, withAll, without and withoutAll as instance methods directly on our mutable collections. Instead of returning boolean like add or remove, these methods return this, which is the collection that the method is operating on. These methods have good symmetry with the existing methods on Collection that return boolean, and also with each other.

We extended this pattern to our immutable collections as well.

ImmutableSet<String> set =
Sets.immutable.with("1", "2", "3")
.newWithAll(Lists.mutable.with("4"))
.newWithoutAll(Lists.mutable.with("1", "3"));
Assert.assertEquals(Sets.mutable.with("2", "4"), set);

In the mutable case, the withAll and withoutAll methods mutate the existing collection. In the newWithAll and newWithoutAll cases, a new collection is returned each time, thus preserving the immutability of the original collection.

Attack Of the Clones

The preposition of lost the battle of the instance-based collection factory methods in Eclipse Collections, because there is no good natural opposite for of like there is for with. That said, of is sometimes an important part of other method names in the Eclipse Collections API.

// Bag API - occurrencesOf
MutableBag<String> bag = Bags.mutable.with("1", "2", "3");
Assert.assertEquals(1, bag.occurrencesOf("2"));
// List API - indexOf
MutableList<String> list = Lists.mutable.with("1", "2", "3");
Assert.assertEquals(1, list.indexOf("2"));
// RichIterable API - sumOfInt, sumOfLong, sumOfFloat, sumOfDouble 
MutableList<String> list = Lists.mutable.with("1", "2", "3");
long sum = list.sumOfInt(Integer::parseInt);
Assert.assertEquals(6L, sum);
// RichIterable API - selectInstancesOf
MutableList<String> list = Lists.mutable.with("1", "2", "3");
MutableList<String> filtered = list.selectInstancesOf(String.class);
Assert.assertEquals(list, filtered);

Revenge of the With

With methods in the RichIterable interface

With became more prevalent in the Eclipse Collections APIs when it was used to augment existing APIs like select, reject, collect, etc. The With methods in the RichIterable API were originally added as optimizations. They allowed us to make anonymous inner classes static, by providing more opportunities to make them completely stateless. As a completely independent and accidental benefit, the With methods provide more opportunities for us to use Method References with Eclipse Collections APIs. This is a good thing, because I have a Method Reference Preference. Here are some examples of using some of these methods with Method References using the domain from the Eclipse Collections Pet Kata.

boolean any =
this.people.anySatisfyWith(Person::hasPet, PetType.CAT);
Assert.assertTrue(any);

boolean all =
this.people.allSatisfyWith(Person::hasPet, PetType.CAT);
Assert.assertFalse(all);

boolean none =
this.people.noneSatisfyWith(Person::hasPet, PetType.CAT);
Assert.assertFalse(none);

Person found =
this.people.detectWith(Person::hasPet, PetType.CAT);
Assert.assertNotNull(found);

int count =
this.people.countWith(Person::hasPet, PetType.CAT);
Assert.assertEquals(2, count);

MutableList<Person> selected =
this.people.selectWith(Person::hasPet, PetType.CAT);
MutableList<Person> rejected =
this.people.rejectWith(Person::hasPet, PetType.CAT);
PartitionMutableList<Person> partition =
this.people.partitionWith(Person::hasPet, PetType.CAT);
Assert.assertEquals(selected, partition.getSelected());
Assert.assertEquals(rejected, partition.getRejected());

Good API design is hard, because naming is hard. It is a great feeling when you discover and use a name that communicates intent clearly to other developers. The best way to do that, is to run your names by other developers you work with you to get a consensus before settling on a name. On very rare occasions where a consensus is not possible (e.g. two equally good alternatives), either just pick a winner or take the cost of providing both. My preference is almost always to just pick a winner and move on. Providing both of and with factory methods will hopefully be a rare exception.

I hope you found this blog helpful. If you liked the blog, claps are appreciated.