Tupel

Doch, das ist ein Katamorphismus. Für einen algebraischen Datentyp wird dazu einfach jeder Konstruktor durch eine allgemeine Funktion der gleichen Arität und mit einheitlichem Rückgabetyp ersetzt.

  • Für List haben wir die Konstruktoren Cons(a,b) und Nil, also fold((a,b)->r, ()->r)
  • Für Optional haben wir (zumindest konzeptionell) die Konstruktoren Some(a) und None, also fold(a->r, ()->r)
  • Für Either haben wir die Konstruktoren Left(a) und Right(b), also fold(a->r, b->r)
    Nach diesem Schema folgt für ein 2-Tupel mit dem Konstruktor Pair(a,b) die Funktion fold((a,b)->r)

Bezüglich der Listen stimme ich dir zu.

Ich meinte das eigentlich nicht auf den Katamorphismus bezogen, sondern auf den Methodennamen fold.
Aber ich stecke in den ganzen Konzepten aus der funktionalen Programmierung nicht drin. Das mit dem Patternmatching habe ich verinnerlicht, aber was sinnvolle weitere Konzepte und vor allem Hintergründe angeht, bin ich ein unbeschriebenes Blatt.

Mal eine neue Idee an der Tupel-Front: Ein naheliegendes Einsatzgebiet für Tuples wären Hilfsmethoden, um für die erweiterte for-Schleife so etwas Ähnliches wie List-Comprehensions realisieren zu können. Die Syntax wäre dann etwa so:

List<Integer> ints = ...
List<String> strings = ...
for(Pair<Integer, String> pair : combine(filter(ints, x -> x % 2 == 0), strings)) {
   ...
}

Die Methoden würden alle auf Iterable-Level arbeiten. Hier ein paar nützliche Signaturen:

//Kreuzprodukt a.k.a. Kombinationen
public static <A, B> Iterable<Pair<A, B>> combine(Iterable<A> itA, Iterable<B> itB);
public static <A, B, C> Iterable<Triple<A, B, C>> combine(Iterable<A> itA, Iterable<B> itB, Iterable<C> itC);
public static <A, B, C, D> Iterable<Quadruple<A, B, C, D>> combine(Iterable<A> itA, Iterable<B> itB, Iterable<C> itC, Iterable<D> itD);

public static <A, B, C> Iterable<C> combineWith(Iterable<A> itA, Iterable<B> itB, , BiFunction<A,B,C> fn);
public static <A, B, C, D> Iterable<D> combineWith(Iterable<A> itA, Iterable<B> itB, Iterable<C> itC, TriFunction<A,B,C,D> fn);
public static <A, B, C, D, E> Iterable<E> combineWith(Iterable<A> itA, Iterable<B> itB, Iterable<C> itC, Iterable<D> itD, QuadFunction<A,B,C,D,E> fn);

//Parallelverkettung
public static <A, B> Iterable<Pair<A, B>> zip(Iterable<A> itA, Iterable<B> itB);
public static <A, B, C> Iterable<Triple<A, B, C>> zip(Iterable<A> itA, Iterable<B> itB, Iterable<C> itC);
public static <A, B, C, D> Iterable<Quadruple<A, B, C, D>> zip(Iterable<A> itA, Iterable<B> itB, Iterable<C> itC, Iterable<D> itD);

public static <A, B, C> Iterable<C> zipWith(Iterable<A> itA, Iterable<B> itB, BiFunction<A,B,C> fn);
public static <A, B, C, D> Iterable<D> zipWith(Iterable<A> itA, Iterable<B> itB, Iterable<C> itC, TriFunction<A,B,C,D> fn);
public static <A, B, C, D, E> Iterable<E> zipWith(Iterable<A> itA, Iterable<B> itB, Iterable<C> itC, Iterable<D> itD, QuadFunction<A,B,C,D,E> fn);

//Hilfsmethoden
public static <A> Iterable<A> filter(Iterable<A> itA, Predicate<A> predA);
public static <A, B> Iterable<B> map(Iterable<A> itA, Function<A, B> fn);
public static <A, B> Iterable<B> flatMap(Iterable<A> itA, Function<A, ? extends Iterable<B>> fn);
public static <A> Iterable<A> flatten(Iterable<? extends Iterable<A>> nested);
public static <A> Iterable<A> array(A ... a); //Array als Iterable verwenden
public static <A> Iterable<A> optional(Optional<A> a); //Optional als Iterable verwenden

Was meint ihr dazu?

Definitiv äußerst nützlich. Parallele Iteration brauche ich in “echten” Projekten hin und wieder mal. Und dafür dann indexbasierten Zugriff verwenden zu müssen oder die Iteratoren manuell zu synchronisieren ist… lästig.
Die Implementierung dazu sollte auch nahezu trivial sein, wenn da nicht noch irgendwelche Stolpersteine liegen, die mir jetzt ad hoc nicht auffallen.

Apropos, wie wäre es damit:

public static <A> Iterable<Pair<A, Integer>> indexed(Iterable<A> itA);

Sicher auch in manchen Fällen sinnvoll, allerdings fände ich es von der Schreibweise der Typdeklaration her intuivier, wenn der Index zuerst käme, also Pair<Integer, A>. Allerdings ginge dann die Gewichtung, dass der Inhalt wichtiger ist, ein wenig verloren, weil der Inhalt über get2 geholt werden müsste.

Da isses: https://github.com/neco4j-team/neco4j/blob/experimental/src/main/java/org/neco4j/tuple/Comprehension.java

Bei der Reihenfolge habe ich mich an Scala orientiert: List("a", "b", "c").zipWithIndex = List(("a", 0), ("b", 1), ("c", 2)) ( Scala Standard Library 2.11.5 )

Bei den ganzen anonymen Klassen mal eine Frage. Wie wirkt sich das eigentlich auf den Speicherverbrauch der JVM aus? Wann darf da wieder aufgeräumt werden?

Natürlich verbrauchen die Objekte etwas Speicher, und natürlich können sie freigegeben werden, wenn niemand mehr eine Referenz darauf hält. Aber die Collection-Klassen tun genau dasselbe. Wenn du z.B. java.util.Map.entrySet() aufrufst, hast du auch ein Iterable über einen „pair-artigen“ Typ.

Selbstverständlich. Mir ging es eher um den Speicherverbrauch der Klassen, wie die im Speicher abgelegt werden. Landen die anonymen Klassen auch im Metaspace? Wird pro Instanz auch eine neue Klasse erzeugt, weil sie anonym ist?

Soweit ich weiß ist eine anonyme Klasse für die JVM einfach eine innere oder statische Klasse, weil der Compiler sie so “übersetzt”. Probiere es doch aus: Wenn du in einer Klasse Foo eine anonyme Klasse verwendest, hast du nach dem Kompilieren neben Foo.class auch Foo$1.class auf der Platte, und die wird dann falls nötig eben geladen - und zwar einmalig, wie alle anderen Klassen auch.

Das muss ich mal ausprobieren, klingt aber plausibel.

*** Edit ***

Ich habe gerade mal getestet, wie sich das verhält. Eine anonyme innere Klasse wird als ganz normale innere Klasse kompiliert. Es wird also bei einer anonymen inneren Klasse in der Klasse Dummy eine Klasse Dummy$1 erzeugt, die als erstes Konstruktorargument die Referenz auf das äußere Objekt bekommt.
Interessant ist aber, dass es bei einem Lambdaausdruck anders aussieht. Dort wird keine innere Klasse erstellt, sondern das Lambda liegt auch als Lambda im Bytecode (habe einfach dekompilieren lassen). Speichermäßig wird also wohl auch keine Klasse erzeugt, sondern „irgendwie einfach mit dem Lambda gearbeitet“.

Lambdas und Methodenreferenzen werden meines Wissens nach vom Compiler als invokedynamic-Aufrufe umgesetzt, wobei ich die Details nicht kenne.

Das haut IMHO nur hin wenn es wirklich ein Lambda ist und kein „Closure“, dann sollte das als statische Methode in der Klasse landen. Für gefangene Variablen usw. wird dann IMHO die LambdaMetaFactory bemüht die dann ein komplettes Objekt zur Laufzeit erzeugt. Die JLS legt da leider nichts fest, deshalb ist es abhängig von der verwendeten JVM wie sich das auf „Speicher“ usw. auswirkt.

Und etwas zur Benennung, könnt ihr die Tupel nicht einfach Tupel2, Tupel3, Tupel4 usw. nennen? Ich weiß es mag cool sein die Dinger quadruple, quintuple, sextuple, octuple usw. zu nennen, aber denkt dabei auch an die Unterstützung der Programmierwerkzeuge wie Codecompletion usw.

Bei Funktionen mit den vorgegebenen Klassen Function und BiFunction war es naheliegend, mit TriFunction und QuadFunction weiterzumachen. Dann bei Tupeln zu numerieren sähe etwas komisch aus (obwohl ich da keine ausgeprägte Meinung habe).