It's no secrete that one of the most common rants about java is that it's so damn verbose.. so I decided one day to toy with writing some simple code to help myself with this problem. I wanted to have something like python's comprehension expressions. so I focused on capturing the main aspect which I feel makes them useful, which is that they condense 4 logical operations into a single expression -
- creating a new container to hold the result
- iterating over every element in the original container
- filtering (if you want)
- potentially applying some function before the elements are inserted to the result container.
so if in java you would write
Set<Integer> numbers = Set.of(1,2,3,4,5,6,7,8); // assuming you have this set of numbers
// Pre java 8
ArrayList<Integer> evensSquared = new ArrayList<>();
for (Integer i: numbers){
if (i % 2 == 0)
evens.add(i*i);
}
// Post java 8
List<Integer> evensSquared = numbers.stream().filter(i -> i % 2 == 0).map(x -> x * x).collect(Collectors.toList());
in python you could just
evens_squared = {x * x for x in numbers if x % 2 == 0}
now, to be honest this simple example is not really that horrible. And even with some more realistic examples that span a few more lines the big problem is not that it would take me another 20 seconds to type the code in java, rather that in general the language feels so inelegant that after writing a file or few I'd often be looking at the code displeased, spending a few more hours refactoring just to make it feel less clunky. And this can be a real time spender.
So I've written some code to try my version of comprehensions
import java.util.ArrayList;
import java.util.Collection;
import java.util.Iterator;
import java.util.Objects;
import java.util.function.Function;
import java.util.function.Predicate;
// naive implementation, can be made better of course
public abstract class Comprehension<T,K, TCollection extends Collection<T>, KCollection extends Collection<K>>{
protected abstract TCollection getSinkAsTCollection();
protected abstract KCollection getSinkAsKCollection();
TCollection comprehend(Iterator<T> source){
return collectionComprehension(source, x -> true, Function.identity(), getSinkAsTCollection());
}
TCollection comprehend(Iterator<T> source, Predicate<T> filter){
return collectionComprehension(source, filter, Function.identity(), getSinkAsTCollection());
}
KCollection comprehend(Iterator<T> source, Predicate<T> filter, Function<T,K> action){
return collectionComprehension(source, filter, action, getSinkAsKCollection());
}
public static <T,K,U extends Collection<K>> U collectionComprehension(Iterator<T> source, Predicate<T> filter, Function<T,K> action, U sink) {
Objects.requireNonNull(source);
Objects.requireNonNull(action);
Objects.requireNonNull(sink);
ArrayList<K> transformedElements = new ArrayList<>(); // accumulating elements into this list first to avoid a ConcurrentModificationException in case source and sink are backed by the same structure
filter = filter == null? x->true: filter;
for (Iterator<T> it = source; it.hasNext(); ) {
T t = it.next();
if (filter.test(t))
transformedElements.add(action.apply(t));
}
sink.addAll(transformedElements);
return sink;
}
public static <T,K,U extends Collection<K>> U collectionComprehension(Iterable<T> source, Predicate<T> filter, Function<T,K> action, U sink) {
return collectionComprehension(source.iterator(), filter, action, sink);
}
}
and two examples for concrete comprehension classes:
package Conversions;
import java.util.HashSet;
public class SetComprehension<T,K> extends Comprehension<T,K, HashSet<T>,HashSet<K>> {
@Override
protected HashSet<T> getSinkAsTCollection() {
return new HashSet<>();
}
@Override
protected HashSet<K> getSinkAsKCollection() {
return new HashSet<>();
}
}
import java.util.ArrayList;
public class ListComprehension<T,K> extends Comprehension<T,K, ArrayList<T>,ArrayList<K>> {
@Override
protected ArrayList<T> getSinkAsTCollection() {
return new ArrayList<>();
}
@Override
protected ArrayList<K> getSinkAsKCollection() {
return new ArrayList<>();
}
}
At this point you could write (in keeping with the previous example)
List<Integer> evensSquared = new ListComprehension<Integer,Integer>().comprehend(numbers.iterator(), x -> x % 2 == 0, x -> x * x);
but compare this with the stream version, this new take is even worse!..
List<Integer> evensSquared = numbers.stream().filter(num -> num % 2 == 0).map(x -> x * x).collect(Collectors.toList());
To shorten this to anything idiomatic you'd need to supply static methods to alleviate the need for the class instantiation ceremony, iterator() and type qualifiers. so you start adding some static methods to the ListComprehension class...
public static <T> List<T> list(Iterable<T> source) {
return new ListComprehension<T,T>().comprehend(source.iterator());
}
public static <T> List<T> list(Iterable<T> source, Predicate<T> filter) {
return new ListComprehension<T,T>().comprehend(source.iterator(), filter);
}
public static <T,K> List<K> list(Iterable<T> source, Predicate<T> filter, Function<T,K> action) {
return new ListComprehension<T,K>().comprehend(source.iterator(),filter,action);
}
and now it works great! nice and short!
List<Integer> evensSquared = list(integers, num -> num % 2 == 0, x -> x * x);
However, now you need to handle a different problem - if you want to support even just the basic java containers as input you would need to write no less than a mind boggling 45(!!!) such overloads. How come? because java has fragmented itself into no less than 6!! different container types, with no common denominator to any of them:
- Iterable
- Iterator (which is not iterable)
- Stream (which is not iterable)
- BaseStream: IntStream, DoubleStream, LongStream (all of which are neither iterable nor a stream)
- Object Array (again, is none of the above)
- Primitive Array - with 8 types of primitives.. and not a subtype of any other option
So in total you have 15 types of container * 3 signatures = 45 static functions.. for each type of Comprehension you would want to make.
So to sum, my questions are:
- what do you think about the api? I obviously recon why java haven't done that itself in it's streams as it would explode the Streams api, but it's just really handy to have a concise way of converting any container to the common ones without the usual ceremony.
- is there any common denominator to at least some of the above types that I missed, or any other method I can use to reduce the number of overloads you would need to cover the conversions between at least some of those types?
- Are there any additional java built containers that I missed and aren't covered by any of the above signatures?
- Are there any commonly used java libraries with container types that aren't covered by any of the above signatures? for example I think google's Guava collections are Iterable so they should be covered, but vavr's collections are rather Traversable so would need additional static overrides
- have any library already done this painstaking job? feels like a job for code generators
Thanks a bunch
P.S - Since each input type has 3 signatures I could of course drop the partial ones and leave only the overloads that have all parameters supplied(i.e send params such as filter=x->true and action=x->x when no filter/mapping is needed).. but the whole point was making these expressions as terse as possible