In a recent
post, I was talking about a micro DSL to create a simple "find
x in a list" service. The key thing here is that it defines how to look for
x in the list. So the list can be a list of anything, not just a list of
x's.
Just to recap then, to find something in a list, the original client code (using a static import) looks like this.
find(needle).in(haystack)
The class (in this case
NeedleFinder) implements the DSL and specifically decides in the in method how to compare a
Needle object to whatever is in the
haystack list. I wanted to create a more generic class so started to implement the
ListFinder to use generics and a couple of interesting things came out.
The generified class looks like this.
public final class ListFinder<T, L> {
private final T target;
private List<L> list = new ArrayList<L>();
private ListFinder(T target) {
this.target = target;
}
public static <T, L> ListFinder<T, L> find(T target) {
return new ListFinder<T, L>(target);
}
public ListFinder<T, L> in(List<L> list) {
this.list = new ArrayList<L>(list);
return this;
}
public L using(Comparator<T, L> comparator) {
for (L item : list) {
if (comparator.equals(target, item)) {
return item;
}
}
return null;
}
public interface Comparator<T, L> {
boolean equals(T target, L item);
}
}
With the following test case showing its usage (the Needle and Bale class aren't show for brevity).
public class ListFinderTest {
private final Needle needle = new Needle("Bob");
private final Needle missing = new Needle("Billy");
private final Bale bale1 = new Bale("Christian");
private final Bale bale2 = new Bale("Bob in disguise");
private final Bale bale3 = new Bale("Kelly");
private final List<Bale> haystack = asList(bale1, bale2, bale3);
private final ListFinder.Comparator<Needle, Bale> comparator = new ListFinder.Comparator<Needle, Bale>() {
@Override
public boolean equals(Needle needle, Bale bale) {
return bale.name.contains(needle.name);
}
};
@Test
public void needleFoundInHaystack() throws Exception {
assertThat(find(needle).in(haystack).using(comparator), is(bale));
}
@Test
public void needleNotFoundInHaystack() throws Exception {
assertThat(find(missing).in(haystack).using(comparator), is(nullValue()));
}
private static ListFinder<Needle, Bale> find(Needle value) {
return ListFinder.find(value);
}
}
Here we're defining the equality of a
Needle in a list of
Bale objects to be when the name of a
Needle is contained in the name of the
Bale. A silly example I know but it illustrates that we redefine what we mean by equality for the list finder by implementing the
ListFinder.Comparator. The concrete example that spawned the idea was when searching for a
Race object inside a list of
Event objects; two completely different entities.
Anyway, what I thought was interesting about this example was the type inference going on in the static
find method. I originally wanted to just use
ListFinder.find method directly as in the following.
ListFinder.find(needle).in(haystack).using(comparator)
Where
ListFinder is statically imported. Usually, I'd rely on type inference here to work out that
needle means
T and therefore
T is of type
Needle. However, in the case above, the compiler will complain as the
haystack parameter is not of type
Object. The trick is that the generic method
find in
ListFinder needs to infer
two types (
T and
L) but only has enough information for
T. So it defaults
L to type
Object. grrr damn Java indisputable logic.
The alternative is to use the full notation as follows.
ListFinder.<Needle, Bale>find(needle).in(haystack).using(comparator)
Or (as I've done in the test) use an internal method who's return type gives the compiler enough information to infer both types. I prefer this approach as it makes the DSL expression to find a needle much more readable.
So, Java can't chain methods to infer the types. I didn't really expect it to be able to so, its a bit much to ask for. Although it would be pretty sweet if it could.
One last thing,
Papa Ray was showing me a
Jedi alternative to the finder. If we're lucky, he might blog about it. It seems Jedi offers some measure of insurgency against the proliferation of anonymous inner classes in lieu of closures but in all honestly, I just wanted to get in some big words in before signing off. TTFN.