Sep 03 2008
Blocks: Not Just Twisted Loops/Iterators, part 2
In the previous post in this series, I attempted to demonstrate some of the basic benefits of blocks by showing examples of listing, filtering, mapping, and sorting in Ruby and in Java.
In general, the Java solutions had to do most of the work themselves with their own loop constructs. However the sorting example was a little different. In Java we were able to pass some code into the Collections.sort method using Java’s anonymous inner classes. So anonymous inner classes are somewhat close to Ruby’s blocks, at least as close as we can get.
With Collections.sort, there was a Comparator interface that we implemented in-line. So if we want to see how close we can get with Java, we’ll have to create our own interface(s) for the methods we want to supply in-line and a class to hold the main logic in a series of class (i.e., static) methods. We’ll create that infrastructure once and then be able to use it repeatedly.
To outline the plan, we will do two things: create an interface for each type of function we’ll be performing on a collection, and create a class — Iterations — to hold the static utility methods that perform the basic functionality.
Recall that for comparison purposes we had Ruby and Java versions of our Account class and a way to generate test accounts. You can get those details in the previous post.
Listing
Let’s start out with the listing task. We’ll first need our infrastructure — an interface that supports doing something to each element of a collection.
public interface EachFunction<T>
{
public void perform(T value);
}
Note that we are using Java’s generics, so this will work in a “type-safe” manner as possible.
And in our Iterations class, we’ll add the following method:
public static<T> void each(Collection<T> inputs, EachFunction<T> function)
{
for (T value : inputs) {
function.perform(value);
}
}
As you can see, the each method takes a collection and a class (in our example we’ll use an anonymous inner class) that implements the EachFunction interface.
With our one-time-only infrastructure in place, we can display a list of all the members in a collection with this code:
Iterations.each(accounts,
new EachFunction<Account>() {
public void perform(Account a) {
System.out.println(a);
}
});
Let’s compare it to our Ruby solution:
accounts.each do |account|
puts account
end
So the concept is the same. However, the syntax requirements is far heavier on the Java side. And, in the case of Ruby, the functionality for each is built into the Array class, where with Java we’re required to create a separate and clunky class full of static methods to house the functionality, which moves us away from a pure object-oriented implementation.
Selecting
We’ll see a similar pattern with the selecting. We’ll start off with our interface that will allow us to convey our selection criteria:
public interface SelectCondition<T>
{
public boolean isSelected(T value);
}
We’ll create our utility method in the Iterations class:
@SuppressWarnings("unchecked")
public static <T> Collection<T> select(Collection<T> inputs,
SelectCondition<T> condition)
{
try {
Collection<T> outputs = inputs.getClass().newInstance();
for (T value : inputs) {
if (condition.isSelected(value)) {
outputs.add(value);
}
}
return outputs;
} catch(Exception e) {
throw new UnsupportedOperationException(e);
}
}
We’re doing something somewhat tricky in creating a new collection to return to the caller. We take the collection passed in, ask for its class, and then call newInstance on it. Because the type information for generics is only kept at compile time — and lost at runtime (due to type erasure) — the runtime types in the assignment cannot be assured. So, to avoid a compiler warning we must add the SupressWarnings annotation.
Also, the call to newInstance can throw either InstantiationException or IllegalAccessException. So the caller is not forced to deal with these exceptions, we’ll catch them ourselves and throw UnsupportedOperationException, which, being a descendent of RuntimeException does not have to be explicitly handled by the caller.
Finally, here’s our code to select those accounts with large balances:
Collection<Account> bigAccounts =
Iterations.select(accounts,
new SelectCondition<Account>() {
public boolean isSelected(Account a) {
return a.balance >= 10000;
}
});
And again, for comparison purposes, here’s the Ruby version:
big_accounts = accounts.select { |account| account.balance >= 10000 }
Mapping
Even though the pattern is pretty clear, for the sake of completeness, and in order to generate a nice package of functionality for Java programmers, let’s complete our task, first with mapping and finally with a Java version of sort_by.
The interface for mapping describes a method that takes input of one type and returns data of a type that can be different:
public interface MapFunction<InputT, OutputT>
{
public OutputT mapOf(InputT value);
}
The utility method in the Iterations class for mapping:
@SuppressWarnings("unchecked")
public static <InputT, OutputT>
Collection<OutputT> map(Collection<InputT> inputs,
MapFunction<InputT, OutputT> mapper)
{
try {
Collection<OutputT> outputs = inputs.getClass().newInstance();
for (InputT value : inputs) {
outputs.add(mapper.mapOf(value));
}
return outputs;
} catch(Exception e) {
throw new UnsupportedOperationException(e);
}
}
And some code that does mapping with the above infrastructure, turning a Collection of accounts to a collection of strings containing the holders’ first names:
Collection<String> firstNames =
Iterations.map(accounts,
new MapFunction<Account, String>() {
public String mapOf(Account account) {
int firstSpace = account.holder.indexOf(' ');
String firstName = account.holder.substring(0, firstSpace);
return firstName;
}
});
And here, again, is the Ruby equivalent:
first_names = accounts.map { |account|
first_space = account.holder.index(' ')
account.holder.slice(0...first_space)
}
Sorting
Ruby collections offer a sort_by method that comes in handy. When the data being sorted is a bundle of data, you can use sort_by to extract part of the bundle as the sorting key. Alternatively, you can calculate new data to sort by.
Interestingly, we do not need a new interface since we can use the same interface we used for mapping. The mapping function will take an element of the collection and return data that will be used as that element’s sort key. And that sort key could be of a different type.
To make this sort_by work, we’ll actually create new data type — ComparablePair — that associates a collection element with its sort key. We put these ComparablePairs into a TreeSet, which is a sorted data structure, and then extract the elements in the sorted order and put them into a collection that is returned to the caller.
So here is our ComparablePair:
static class ComparablePair<SortT extends Comparable<SortT>, DataT>
implements Comparable<ComparablePair<SortT, DataT>>
{
SortT sortValue;
DataT dataValue;
ComparablePair(SortT sortValue, DataT dataValue)
{
this.sortValue = sortValue;
this.dataValue = dataValue;
}
public int compareTo(ComparablePair<SortT, DataT> other)
{
return sortValue.compareTo(other.sortValue);
}
}
And here’s the code that creates the ComparablePairs, puts them in a TreeSet, which sorts the data as it’s inserted, and extracts the data from the TreeSet in sorted order and puts them in a new collection, that is finally returned:
@SuppressWarnings("unchecked")
public static <DataT, SortT extends Comparable<SortT>>
List<DataT> sortBy(List<DataT> inputs,
MapFunction<DataT, SortT> sortMapper)
{
try {
// do this first so exceptions generated as soon as possible
List<DataT> sortedList = inputs.getClass().newInstance();
// sort the data (in pairs) by putting them into a sorted set
TreeSet<ComparablePair<SortT, DataT>> sortedSet =
new TreeSet<ComparablePair<SortT, DataT>>();
for (DataT value : inputs) {
ComparablePair<SortT, DataT> sortable =
new ComparablePair<SortT, DataT>(sortMapper.mapOf(value),
value);
sortedSet.add(sortable);
}
// pull the sorted data out and put in our result list
for (ComparablePair<SortT, DataT> pair : sortedSet) {
sortedList.add(pair.dataValue);
}
return sortedList;
} catch(Exception e) {
throw new UnsupportedOperationException(e);
}
}
With that infrastructure in place, we can now sort the accounts by balance:
Collection<Account> accountsByBalance =
Iterations.sortBy(accounts,
new MapFunction<Account, Double>() {
public Double mapOf(Account a) {
return a.balance;
}
});
or, alternatively by the holder’s name:
Collection<Account> accountsByName =
Iterations.sortBy(accounts,
new MapFunction<Account, String>() {
public String mapOf(Account a) {
return a.holder;
}
});
And here are the two Ruby equivalents for sorting by balance:
accounts_by_balance = accounts.sort_by { |account| account.balance }
and by holder’s name:
accounts_by_holder = accounts.sort_by { |account| account.holder }
Conclusions
I find it interesting that by building a bit of infrastructure in the form of some basic interfaces and a class of utility methods, we can approach the Ruby way in Java. It’s not perfect, though. The syntax is not as nice, it’s a bit clunky to have to put all this code in a utility class, and we do have to suppress some compiler warnings. However I’m pleased with the result. It does show that Java has some features worth appreciating, but it also makes clear, I think, the role of Ruby’s blocks along with their benefits.
If you would like to see the full source code for these examples, the Ruby source and the Java source code can be downloaded as zip files.
