zipWith in Java 8

Heute einmal ein ziemlich einfaches Beispiel, wie Lambdas das Leben in Java 8 leichter machen. In Haskell und Scala gibt es die Funktione zipWith, mit der zwei Datenstrukturen wie Listen durch elementweise Verknüfung zu einer neuen „zusammengeklebt“ werden. Dabei muss man aufpassen: Ist eine der Ausgangsstrukturen länger als die andere, werden die „überflüssigen“ Elemente einfach ignoriert. In Java bietet sich so eine Funktion an mindestens zwei Stellen an: Bei Iterables und bei den neuen Streams. Da ich mich mit letzteren (noch) nicht so gut auskenne, will ich heute den einfacheren ersten Fall behandeln.

Ein besonders nützlicher Anwendungsfall für zipWith ist, wenn man mit der erweiterten for-Schleife zwei Collections gleichzeitig durchgehen will – vorher musste man meist auf andere Sprachmittel (z.B. Indexe oder Iteratoren) ausweichen. Wie könnte nun so eine Schleife aussehen?

List<String> strings = Arrays.asList("a","b","c");
List<Integer> ints = Arrays.asList(6,9,14,32);
for(String result : zipWith(strings, ints, (s,i) -> s + i)) {
   System.out.println(result);
}

Das erwartete Ergebnis wären hier die Zeilen „a6“, „b9“ und „c14“. Natürlich wäre auch eine anonyme Klasse an Stelle des Lambda-Ausdrucks möglich gewesen, aber erst durch diesen wird das ganze Konstrukt lesbar. Die Umsetzung ist trivial:

import java.util.function.BiFunction;
...
public static <A,B,C> Iterable<C> zipWith(Iterable<A> iterableA, Iterable<B> iterableB, BiFunction<A,B,C> fn) {
    return () -> new Iterator<C>() {
        private Iterator<A> itA = iterableA.iterator();
        private Iterator<B> itB = iterableB.iterator();

        public boolean hasNext() {
            return itA.hasNext() && itB.hasNext();
        }

        public C next() {
            return fn.apply(itA.next(), itB.next());
        }
    };
}

Wer sich wundert, wo das „new Iterable“ geblieben ist: Da das Interface nur eine Methode (nämlich iterator()) besitzt, können wir es durch einen Lambda-Ausdruck ersetzen. Wir brauchen auch die remove-Methode von Iterator nicht zu implementieren, es gibt in Java 8 eine Default-Methode dafür (die eine UnsupportedOperationException wirft). Und als letztes fällt auf, dass wir aus dem Lambda-Ausdruck heraus auf iterableA und iterableB zugreifen konnten, ohne dass wir diese final machen mussten. Da beide Argumente nicht (weder in der Methode noch im Lambda-Ausdruck) verändert werden, sind sie „effektiv final“ und benötigen keinen entsprechenden Modifikator.

Das war jetzt etwas leichtere Kost, aber ich hoffe trotzdem ein wenig nützlich.

Wer weiß, wie man das Gleiche mit Streams anstellt, darf seine Lösung hier gerne vorstellen, ich bin gespannt darauf…

[Update]

Für Streams habe ich auf Stackoverflow diesen Schnipsel gefunden:

public static<A, B, C> Stream<C> zip(Stream<? extends A> a,
                                     Stream<? extends B> b,
                                     BiFunction<? super A, ? super B, ? extends C> zipper) {
    Objects.requireNonNull(zipper);
    @SuppressWarnings("unchecked")
    Spliterator<A> aSpliterator = (Spliterator<A>) Objects.requireNonNull(a).spliterator();
    @SuppressWarnings("unchecked")
    Spliterator<B> bSpliterator = (Spliterator<B>) Objects.requireNonNull(b).spliterator();

    // Zipping looses DISTINCT and SORTED characteristics
    int both = aSpliterator.characteristics() & bSpliterator.characteristics() &
            ~(Spliterator.DISTINCT | Spliterator.SORTED);
    int characteristics = both;

    long zipSize = ((characteristics & Spliterator.SIZED) != 0)
            ? Math.min(aSpliterator.getExactSizeIfKnown(), bSpliterator.getExactSizeIfKnown())
            : -1;

    Iterator<A> aIterator = Spliterators.iterator(aSpliterator);
    Iterator<B> bIterator = Spliterators.iterator(bSpliterator);
    Iterator<C> cIterator = new Iterator<C>() {
        @Override
        public boolean hasNext() {
            return aIterator.hasNext() && bIterator.hasNext();
        }

        @Override
        public C next() {
            return zipper.apply(aIterator.next(), bIterator.next());
        }
    };

    Spliterator<C> split = Spliterators.spliterator(cIterator, zipSize, characteristics);
    return (a.isParallel() || b.isParallel())
           ? StreamSupport.stream(split, true)
           : StreamSupport.stream(split, false);
}

Der Original-Code war wohl in der Stream-Implementierung im Lambda-Projekt dabei und ist später unverständlicherweise rausgeflogen.

Werbeanzeigen

Gezippt und zugenäht

In diesem Blog kommen regelmäßig Tupel vor, und normalerweise wird um diese einfach zu verstehenden, nützlichen Klassen nicht allzuviel Aufhebens gemacht. Heute will ich einmal näher beleuchten, wie Tupel und die Scala-Collections so zusammenpassen – nämlich prächtig.

Zunächst wären da Methoden, die eine Collection in zwei Teile zerlegen:

List(1,2,3,4,5).partition(_ % 3 == 1)
res6: (List[Int], List[Int]) = (List(1, 4),List(2, 3, 5))

List(1,2,3,4,5).span(_ % 3 == 1)
//--> res1: (List[Int], List[Int]) = (List(1),List(2, 3, 4, 5))

List(1,2,3,4,5).splitAt(2)
//--> res2: (List[Int], List[Int]) = (List(1, 2),List(3, 4, 5))

Die Methode partition teilt die Collection nach einem gegeben Kriterium auf, während span nur solange Werte in die erste Liste packt, bis das Kriterium das erste mal falsch wird. Die Methode splitAt teilt die Collection an einem gegebenen Index. Natürlich kann man das Tupel schon bei der Zuweisung gleich wieder aufdröseln:

val (rest1,rest02) = List(1,2,3,4,5).partition(_ % 3 == 1)
//--> rest1: List[Int] = List(1, 4)
//--> rest02: List[Int] = List(2, 3, 5)

Nützlich sind auch die zip-Methoden:

List(1,2,3,4) zip List("A","B","C")
//--> res7: List[(Int, java.lang.String)] = List((1,A), (2,B), (3,C))

List("null","eins","zwei","drei").zipWithIndex
//--> res8: List[(java.lang.String, Int)] = List((null,0), (eins,1), (zwei,2), (drei,3))

List((1,"A"), (2,"B"), (3,"C")) unzip
res9: (List[Int], List[java.lang.String]) = (List(1, 2, 3),List(A, B, C))

Die Methode zip packt zwei Collections paarweise zusammen. Aber Achtung: Wie man im obigen Beispiel sieht, werden überzählige Elemente (hier die 4) stillschweigend weggeworfen.
In Java hat man oft das Problem, dass man statt der erweiterten for-Schleife doch wieder zur altgewohnten Schreibweise zurückkehren muss, weil man einen Index braucht. Scalas Antwort auf dieses Problem ist so simpel wie elegant: zipWithIndex packt die benötigten Indizes einfach zu den Elementen dazu.
Eine Liste aus Paaren bekommt man mit unzip wieder getrennt, aus einer Liste mit Paaren wird also ein Listen-Paar.

Stellt sich zu guter Letzt die Frage nach dem elegantesten Zugriff auf so eine gepaarte Liste. Natürlich kann man einfach das Tupel nehmen und mit den Unterstrich-Methoden auf dessen Elemente zugreifen:

List("null","eins","zwei","drei").zipWithIndex.map(t => t._1 + "=" + t._2)
//--> res10: List[java.lang.String] = List(null=0, eins=1, zwei=2, drei=3)

Eleganter ist die Verwendung von case, das allerdings geschweifte Klammern erfordert:

List("null","eins","zwei","drei").zipWithIndex.map{case(s, i) => s + "=" +  i}
//--> res11: List[java.lang.String] = List(null=0, eins=1, zwei=2, drei=3)

Will man mit den beiden Elementen des Tupels eine Funktion aufrufen, kann man sich sogar das Zerlegen sparen – man wandelt stattdessen die zweielementige Funktion in eine Funktion um, die ein Tupel als Argument nimmt. Berechnen wir einmal so die Hypothenuse von ein paar rechtwinkligen Dreiecken:

 
List((3.0,4.0),(5.0,12.0)).map(math.hypot _ tupled)
//--> res14: List[Double] = List(5.0, 13.0)

Die Methode hypot im math-Package nimmt als Parameter die beiden Katheden. Durch den Aufruf der Methode tupled an hypot wird daraus eine Funktion, die ein Tupel von zwei Doubles erwartet. Der Unterstrich hier mag erst einmal etwas mysteriös erscheinen, aber die Erklärung ist ganz einfach: Es ist ein Hinweis, dass wir hypot an dieser Stelle nicht ausführen wollen, sondern „als Funktion an sich“ (Kant läßt grüßen) ansprechen. Woher soll auch sonst der Compiler wissen, dass es sich bei tupled nicht um ein Argument handelt, sondern eine Methode, die wir an hypot aufrufen wollen? Zur Verwirrung trägt sicher bei, dass man an dieser Stelle den Unterstrich eher für die Kurzschreibweise für Funktionen (also tuwas(_) anstatt x => tuwas(x)) erwarten würde.

Übrigens gibt es auch die „entgegengesetzte“ Methode untupled, mit der man aus einer Funktion func, die ein Tupel als Argument hat, eine macht, die die Argumente einzeln erwartet, allerdings muss man dazu Function.untupled(func _) schreiben.

So, genug herumgetupelt…