I've implemented functional unzip()
operation as follows:
public static <T, U, V> Tuple2<Stream<U>, Stream<V>> unzip(
Stream<T> stream,
Function<T, Tuple2<U, V>> unzipper) {
return stream.map(unzipper)
.reduce(new Tuple2<>(Stream.<U>empty(), Stream.<V>empty()),
(unzipped, tuple) -> new Tuple2<>(
Stream.concat(unzipped.$1(), Stream.of(tuple.$1())),
Stream.concat(unzipped.$2(), Stream.of(tuple.$2()))),
(unzipped1, unzipped2) -> new Tuple2<>(
Stream.concat(unzipped1.$1(), unzipped2.$1()),
Stream.concat(unzipped1.$2(), unzipped2.$2())));
}
This works fine, given input streams don't have a lot of elements. This is because accessing an element of a deeply concatenated stream can cause StackOverflowException
. According to the docs of Stream.concat()
:
Implementation Note:
Use caution when constructing streams from repeated concatenation. Accessing an element of a deeply concatenated stream can result in deep call chains, or even
StackOverflowException
.
For few elements, my unzip
implementation works. Given a class Person
:
class Person {
public final String name;
public final int age;
Person(String name, int age) {
this.name = name;
this.age = age;
}
}
If I have a stream of people:
Stream<Person> people = Stream.of(
new Person("Joe", 52),
new Person("Alan", 34),
new Person("Peter", 42));
I can use my unzip()
implementation this way:
Tuple2<Stream<String>, Stream<Integer>> result = StreamUtils.unzip(people,
person -> new Tuple2<>(person.name, person.age));
List<String> names = result.$1()
.collect(Collectors.toList()); // ["Joe", "Alan", "Peter"]
List<Integer> ages = result.$2()
.collect(Collectors.toList()); // [52, 34, 42]
Which is correct.
So my question is: is there a way for unzip()
to work with many elements (potentially infinite)?
Note: for completeness, here's my immutable Tuple2
class:
public final class Tuple2<A, B> {
private final A $1;
private final B $2;
public Tuple2(A $1, B $2) {
this.$1 = $1;
this.$2 = $2;
}
public A $1() {
return $1;
}
public B $2() {
return $2;
}
}