Preposition Preference
What’s up? A preposition.
A friend of mine at Rutgers University would always respond to the question “What’s up?” with the consistent 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 methods 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 a method name, 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 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");
Assertions.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"));
Assertions.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"));
Assertions.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");
Assertions.assertEquals(1, bag.occurrencesOf("2"));
// List API - indexOf
MutableList<String> list = Lists.mutable.with("1", "2", "3");
Assertions.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);
Assertions.assertEquals(6L, sum);
// RichIterable API - selectInstancesOf
MutableList<String> list = Lists.mutable.with("1", "2", "3");
MutableList<String> filtered = list.selectInstancesOf(String.class);
Assertions.assertEquals(list, filtered);
Revenge of the With
With
became more prevalent in the Eclipse Collections API when it was used to augment existing methods like select
, reject
, collect
, etc. The With
methods in the RichIterable
interface 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 the Eclipse Collections API. 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);
Assertions.assertTrue(any);
boolean all = this.people.allSatisfyWith(Person::hasPet, PetType.CAT);
Assertions.assertFalse(all);
boolean none = this.people.noneSatisfyWith(Person::hasPet, PetType.CAT);
Assertions.assertFalse(none);
Person found = this.people.detectWith(Person::hasPet, PetType.CAT);
Assertions.assertNotNull(found);
int count = this.people.countWith(Person::hasPet, PetType.CAT);
Assertions.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);
Assertions.assertEquals(selected, partition.getSelected());
Assertions.assertEquals(rejected, partition.getRejected());
Update (Feb. 2022): Years after I originally wrote this article, a new API was added to Eclipse Collections called containsBy
. I thought it was interesting that we refactored the hasPet
method on the Person
class in the Pet Kata to use containsBy, using a Method Reference, of course.
public boolean hasPet(PetType petType)
{
return this.pets.containsBy(Pet::getType, petType);
}
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 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.