List und default methods

Später gehe ich nochmal etwas detaillierter auf die Punkte ein (Verschieben von Methoden etc.), jetzt nur ganz kurz:
NecoList (ich glaube, darauf haben wir uns unterschwellig geeinigt?) List implementieren zu lassen, halte ich für nicht so geschickt. Das lenkt zu sehr von unserem Ziel ab und führt zu einem aufgeblasenen Interface. Es wären derzeit 19 Methoden, die noch implementiert werden müssten. Davon hätten 10 keine Funktion.

[QUOTE=Landei]Vorschlag:

  • zip, strictZip, unzip, iterateN, iterateWhile, lefts(), rights(), findFirst erst einmal weglassen / rausschmeißen

  • List umbenennen in „NecoList“

  • „all“ und „any“ in NecoList implementieren [/QUOTE]
    Finde ich gut und habe ich testweise lokal schonmal umgesetzt.

[QUOTE=Landei;95526]- neue Utility-Klasse „NecoLists“

  • „zipWith“, „strinctZipWith“, „toList“, „fromList“ sowie „leftRights“ als „eithers“ in NecoLists verschieben / implementieren[/QUOTE]
    Habe ich lokal auch mal umgesetzt, allerdings kann ich die Namen zipWith und strictZipWith nicht nachvollziehen. Die hätte ich eher bei der nichtstatischen Version genommen. In einer Utilityklasse finde ich liest sich NecoLists.zip(a, b) besser als NecoLists.zipWith(a, b). Wohingegen ich myList.zipWith(otherList) gegenüber myList.zip(otherList) vorziehen würde.
    Da habe ich die Namen also bei „zip“ und „strictZip“ gelassen.
    Das „fromList“ würde ich zu „fromIterable“ verallgemeinern, auch wenn es dann nicht mehr als 1:1 Counterpart für „toList“ erkennbar ist.

Den Namen „flatten“ finde ich auch besser als „unpack“. Allerdings zickt die Type-Erasure da ein bisschen: public static <A> NecoList<A> flatten(NecoList<Optional<A>> list), public static <A> NecoList<A> flatten(NecoList<Iterable<A>> list) und public static <A> NecoList<A> flatten(NecoList<A[]> list) sind nach der Erasure gleich.
Habt ihr bessere Vorschläge als „flattenOptional“, „flattenIterable“ und „flattenArray“?

*** Edit ***

Und zum Verhalten von „fromIterable“ und den flatten-Methoden: sollen die eine NPE werfen, oder Nullwerte einfach herausfiltern? (Ich präferiere die NPE)
Und „toList“ sollte konsequenterweise „asList“ heißen.

*** Edit ***

Die „asList“-Methode einmal zur Diskussion (bisher alles noch ungetestet). Vor allem für den ListIterator ist mir nichts effizientes eingefallen:

    return new NecoListList<>(list);
}

private static class NecoListList<A> extends AbstractList<A> {
    private final NecoList<A> content;

    private NecoListList(NecoList<A> list) {
        content = Objects.requireNonNull(list);
    }

    @Override
    public A get(int index) {
        return content.get(index);
    }

    @Override
    public int size() {
        return content.size();
    }

    @Override
    public Iterator<A> iterator() {
        return content.iterator();
    }

    @Override
    public ListIterator<A> listIterator(int index) {
        return new ListItr(index);
    }

    private class ListItr implements ListIterator<A> {
        private int pos;
        private NecoList<A> forward;
        private NecoList<A> backward;

        private ListItr(int index) {
            pos = 0;
            forward = NecoListList.this.content;
            backward = forward.reverse();
            for (int i = 0; i < index; i++) {
                next();
            }
        }

        @Override
        public boolean hasNext() {
            return !forward.isEmpty();
        }

        @Override
        public A next() {
            if (!hasNext()) {
                throw new NoSuchElementException();
            }
            pos++;
            A value = forward.head();
            forward = forward.tail();
            backward = NecoList.cons(value, backward);
            return value;
        }

        @Override
        public boolean hasPrevious() {
            return !backward.isEmpty();
        }

        @Override
        public A previous() {
            if (!hasNext()) {
                throw new NoSuchElementException();
            }
            pos--;
            A value = backward.head();
            backward = backward.tail();
            forward = NecoList.cons(value, forward);
            return value;
        }

        @Override
        public int nextIndex() {
            return pos + 1;
        }

        @Override
        public int previousIndex() {
            return pos - 1;
        }

        @Override
        public void remove() {
            throw new UnsupportedOperationException();
        }

        @Override
        public void set(A a) {
            throw new UnsupportedOperationException();
        }

        @Override
        public void add(A a) {
            throw new UnsupportedOperationException();
        }
    }

    @Override
    public List<A> subList(int fromIndex, int toIndex) {
        return new NecoListList<>(content.drop(fromIndex).take(toIndex - fromIndex + 1));
    }
}```

Man könnte alternativ eine Implementierung als unverkettete Liste umsetzen, wodurch man dann wahlfreien Zugriff in konstanter Zeit ermöglichen könnte. Vom Speicherplatzverbrauch ist es auch weniger (zumindest, wenn mindestens einmal listIterator aufgerufen wird). Abgesehen von der Initialisierung wäre die Implementierung auch schneller.

*** Edit ***

Noch ein Vorschlag zur Diskussion: ich würde size() als Feld implementieren, das im Konstruktor von Cons initialisiert wird bzw. als fester Rückgabewert in Nil. Die Defaultimplementierung in NecoList würde ich drinlassen.
Folgen:
[ul][li]pro Eintrag in der Liste ein int mehr (also etwa 1/3 mehr Speicherplatzverbrauch)[/li][li]Zugriff auf size() in O(1)[/li][li]cons() immer noch in O(1)[/ul][/li]Das mit dem Speicherplatzverbrauch ist auch relativiert zu sehen, weil sich das ja nur auf den Listenoverhead und nicht auf die Nutzdaten bezieht. Andererseits ist size() auch eine Methode, die häufig genutzt wird.

*** Edit ***

Alternative Implementierung zu asList:
```public static <A> List<A> asList(NecoList<A> list) {
    return Arrays.asList(asArray(list));
}

@SuppressWarnings("unchecked")
public static <A> A[] asArray(NecoList<A> list) {
    Objects.requireNonNull(list);
    int size = list.size();
    Object[] os = new Object[size];
    A[] result = (A[]) os;
    NecoList<A> current = list;
    for (int i = 0; i < size; i++) {
        result** = current.head();
        current = current.tail();
    }
    return result;
}```

Direkt List zu implementieren … da hab’ ich auch ein etwas flaues Gefühl dabei.

Ich hatte ja (auch noch woanders) schon man den Sinn von UnsupportedOperationException an sich in Frage gestellt: Eigentlich sollten geeignete Hierarchien und Strukturen von Interfaces ja genau dafür sorgen, dass man diese Exception nicht braucht. Das Interface sollte ja gerade angeben, welche Methoden es wirklich gibt. Die Begründung, warum das nicht gemacht wurde, steht in Java Collections API Design FAQ und ist zwar nachvollziehbar, aber fühlt sich doch irgendwie „komisch“ an, und es drängt sich das Gefühl eines Mangels an Ausdruckstärke der Sprache auf… -_-

Sicher könnte es an manchen Stellen „praktisch“ sein, direkt List zu implementieren, aber ich denke, dass man es erstmal nicht machen sollte, und ggf. nochmal genau und kritisch drüber nachdenken, wenn sich der Rest etwas festgerüttelt hat.

Hm. Auch wenn ich den Code gerade nicht hier habe, und immernoch nicht den ganzen Überblick: Ich bin da etwas zwiegespalten. Einerseits finde ich, dass zwischen

public static <A> NecoList<A> flatten(NecoList<Optional<A>> list)

und

public static <A> NecoList<A> flatten(NecoList<Iterable<A>> list)
public static <A> NecoList<A> flatten(NecoList<A[]> list)

ein großer „konzeptueller“ Unterschied ist: Im einen Fall werden einzelne Elemente aus einem „Holder“ (dem Optional) rausgeholt - das entspricht für mich eher einem map. Beim anderen werden „Sequenzen“ (also Iterables oder Arrays) aneinandergehängt.

andererseits … ist das ja genau das gleiche. Also, man könnte es ja beides als eine Art map ansehen. Sorry, falls der Begriff nicht stimmt, aber ich hoffe, es ist klar, was gemeint ist, wenn ich das mal so „ins Unreine“ skizziere:

public static <A, T> NecoList<A> flatten(NecoList<T> list, Function<T, NecoList<A>> extractor)

...

NecoList<Optional<String>> optionals = ...
String array[] = ...
List<String> list = ...

NecoList<String> a = NecoLists.flatten(optionals, optionalsExtractor());
NecoList<String> b = NecoLists.flatten(array, arrayExtractor());
NecoList<String> c = NecoLists.flatten(optionals, iterableExtractor());

(wuh, das muss ich wohl doch in „echtem“ Code ausprobieren… :o )

Hm. Vielleicht habe ich da was übersehen, aber mein Gedanke war, dass
[ul]
[li]toList eine neue Liste erstellt, mit dem gleichen Inhalt wie die NecoList
[/li][li]asList eine Ansicht auf die NecoList erstellt. (Diese Liste wäre also unmodifiable)
[/li][/ul]

@cmrudolph Die size-Werte mitzuschleifen halte ich für keine gute Idee. In vielen Fällen kann die Benutzung von size() vermieden werden.

Theoretisch könnte man den Speicher-Overhead durch einen Wrapper vermeiden: Cons und Nil wären dann keine NecoList mehr, sondern nur deren innere Knoten, und NecoList würde seine Größe dann selbst kennen. Das wäre natürlich eine weitreichende Änderung, aber eventuell auch eine Chance, Interface und Implementierung besser voneinander zu entkoppeln. Muss ich nochmal genauer drüber nachdenken.
@Marco13 Die Version von flatten, die du skizziert hast, haben wir schon nicht-statisch als flatMap

[QUOTE=Marco13]Hm. Vielleicht habe ich da was übersehen, aber mein Gedanke war, dass
[ul]
[li]toList eine neue Liste erstellt, mit dem gleichen Inhalt wie die NecoList
[/li][li]asList eine Ansicht auf die NecoList erstellt. (Diese Liste wäre also unmodifiable)
[/li][/ul][/QUOTE]
Du hast schon Recht. Allerdings sehe ich den Nutzen einer toList-Methode nicht. Denn damit ist man direkt an eine Listenimplementierung gekoppelt. Mit einer asList-Methode kann der Entwickler selbst festlegen, welche Implementierung er wählt, indem er z. B. new ArrayList(NecoLists.asList(myList)) macht.

Wie wäre es, wenn NecoList wenigstens Collection implementiert? Das Interface ist recht übersichtlich, und die 6 problematischen Methoden sind “optional” (was man als Entwickler ja auch schon von den Collections.unmodifiableXXX-Methoden gewöhnt sein sollte). Damit ließe sich NecoList in vielen normalen Collection-Methoden wie addAll, removeAll, retainAll und als Konstruktor-Argument verwenden (wobei ich mich schon frage, warum sie dazumal keinen Konstruktor new ArrayList(Iterable<T> it) angeboten haben). Collection ist in vielen praktischen Fällen “gut genug”, es muss oft keine Liste sein.

Hm. Der Unterschied zwischen Collection und List ist so gering, dass das IMHO kaum einen Unterschied machen würde (get, set, add(index,T), indexOf, spontan … und inwieweit man dann noch Informationen darin unterbringen könnte, dass man RandomAccess explizit nicht implementiert, müßte man sich überlegen).

Über die Tatsache, dass überall Collection statt des (meistens ausrechenden) Iterable verwendet werden muss, bin ich auch schon öfter gestolpert - bis hin zu Krämpfen wie

    static <E, C extends Collection<E>> C toCollection(Iterable<E> iterable, C collection)
    {
        for (E e : iterable)
        {
            collection.add(e);
        }
        return collection;
    }
    
...    
Iterable<String> iterable = null;
List<String> list = toCollection(iterable, new ArrayList<String>());

(dass in einigen Fällen ausgenutzt wird, dass die size() bekannt ist und damit das Hinzufügen mit weniger neu-Allokationen/rehashes auskommt, mag ein Punkt sein, aber ein vergleichsweise schwacher…)

Es stimmt schon, dass ein set.addAll(necoList); sehr praktisch wäre. Ich glaube, das ist eine sehr allgemeine Frage. Ähnlich zur Argumentation, dass man toList weglassen könnte, und
List<T> list = new ArrayList(NecoLists.asList(necoList))
schreiben könnte, könnte man ja auch
set.addAll(NecoLists.asList(necoList));
schreiben.

Eine gefestigt-fundierte Meinung habe ich dazu erstmal nicht - beides hat offensichtliche Vor- und Nachteile (die Möglichkeit von set.addAll(necoList.asList()); sei der Vollständigkeit halber hier nochmal erwähnt).

Gibt’s irgendwelche Präferenzen? (Ich will das nicht zu einer Grundsatzfrage im Sinne von “Minimalistisch vs. ‘bequem verwendbar’” erheben, aber es geht in diese Richtung…)

Ich habe jetzt einfach mal die im Post #22 genannten Änderungen eingecheckt.
Allerdings habe ich die komplett offenen Punkte der flatten-Methoden sowie der asList-Implementierung erst einmal weggelassen. Der Code zu den Methoden sieht bei mir so aus:

    return Arrays.asList(asArray(list));
}

@SuppressWarnings("unchecked")
public static <A> A[] asArray(NecoList<A> list) {
    Objects.requireNonNull(list);
    int size = list.size();
    Object[] os = new Object[size];
    A[] result = (A[]) os;
    NecoList<A> current = list;
    for (int i = 0; i < size; i++) {
        result** = current.head();
        current = current.tail();
    }
    return result;
}

public static <A> NecoList<A> flattenOptional(NecoList<Optional<A>> list) {
    return list.foldRight((a, b) -> a.isPresent() ? NecoList.cons(a.get(), b) : b, NecoList.<A>empty());
}

public static <A> NecoList<A> flattenIterable(NecoList<Iterable<A>> list) throws NullPointerException {
    NecoList<A> result = NecoList.empty();
    for (Iterable<A> as : list) {
        for (A a : as) {
            result = NecoList.cons(a, result);
        }
    }
    return result.reverse();
}

public static <A> NecoList<A> flattenArray(NecoList<A[]> list) throws NullPointerException {
    NecoList<A> result = NecoList.empty();
    for (A[] as : list) {
        for (A a : as) {
            result = NecoList.cons(a, result);
        }
    }
    return result.reverse();
}```

*** Edit ***

Ich bin auch gegen die Implementierung des Collection-Interfaces, aus dem selben Grund wie oben bei List schon angeführt. Wie  @Marco13  schon sagte, könnte man auch `set.addAll(NecoLists.asList(necoList));` schreiben.
Was ich eher befürworten würde, wäre die asList-Methode nichtstatisch zu machen und in NecoList einzufügen, sodass die Syntax etwas schlanker wird: `set.addAll(necoList.asList());`. Das halte ich derzeit für den besten Kompromiss aus fokussiertem Interface und guter Nutzbarkeit.

Der Cast in asArray ist natürlich unschön, lässt sich aber leider nicht vermeiden (es sei denn, man muss A[] als Argument übergeben, was hässlich ist).

Nachdem ich endlich mal dazu gekommen bin, zuhause “echt” mit einer IDE in den Code zu schauen, erstmal eine ganz naive Frage (auch wenn ich davon ausgehen muss, dass ich mich damit als “Funktional-Persistenz-Voll-Noob” oute) : Warum gibt es nur “cons”? In den default-Implementierungen wird sehr oft “reverse” aufgerufen. Es mag verfrüht sein, sich über Performance Gedanken zu machen, aber hoffe, dass meine Irritation da nachvollziehbar ist. Sicher wäre beim Gegenteil das “tail()” etwas trickier, aber … (ich bin schon davon ausgegangen, dass das nicht einfach wird, aber es gibt doch sicher schon Lösungen für sowas…!?)

Und dann noch ein… räusper … etwas heikles Thema. rumdrucks. Wie steht ihr zu Dingen wie “JavaDocs”?

Willkommen im Club!

noparse[/noparse] Das Problem ist, dass die Rückwärtsreferenz dazu führt, dass von jedem Element aus jedes andere erreicht werden kann und dadurch beim Hinzufügen eines weiteren Elementes in die Liste eine vollständige Kopie aufgebaut werden muss. Genau das passiert beim reverse ja auch.
Eine alternative Implementierung könnte eine Skiplist sein.
Falls ich mit der Vermutung falsch liege, klärt @Landei das sicherlich auf.

Ich glaube nicht, dass es zu früh ist. Die Aspekte die du ansprichst, sind architekturbedingt. Und das sollte man von vornherein gut eruieren.
Den Punkt mit size hatte ich weiter oben ja auch schon angemerkt und auch das ist mMn ein fundamentaler Punkt. Die Idee die size auszulagern finde ich interessant und glaube, dass sie sich wahrscheinlich sogar relativ problemlos umsetzen lassen sollte. Naiv stelle ich mir das so vor, dass die Implementierung ein Wrapper um Cons und Nil ist, der für die Listenmetainformationen (hier wohl nur size) zuständig ist und aus Cons / Nils werden diese Daten dann entfernt.

Ich finde die Javadocs sehr wichtig, auch wenn ich sie oft sträflich vernachlässige. Bei vielen der List-Methoden ist das Standardverhalten nicht unbedingt eindeutig, was man ja auch im Threadverlauf sieht. So etwas sollte ganz klar aus der Dokumentation hervorgehen. (Beim Programmieren geraten meine Finger fast schon automatisch auf Strg+Q.)
Aber im Code ist das bisher doch konsequent durchgezogen :wink:

Performance ist nicht alles, aber ohne Performance ist alles nichts.

Eine Alternative zu reverse wären rekursive Aufrufe wie:

//NecoList
public default <B> NecoList<B> map(Function<? super A, ? extends B> fn) {
  return cons(fn.apply(head()), map(tail());
}

//Nil
public default <B> NecoList<B> map(Function<? super A, ? extends B> fn) {
  return empty();
}

Kurz, elegant, verständlich und leider völlig unbrauchbar wegen möglichem Stacküberlauf. Etwas anderes ist mit einer strikten einfach verketteten Liste nicht hinzubekommen, sie muss von „hinten“ aufgebaut werden, und wenn die Daten von „vorne“ kommen, braucht man irgend eine Form von Zwischenspeicher (was ja der Aufrufstack der VM im Prinzip auch ist).

Was ebenfalls funktionieren würde, wäre eine „lazy“-Liste, also ein „Stream“ im funktionalen Sinne:

//fiktive Erweiterung von NecoList um einen lazyCons, das einen Supplier für tail akzeptiert
public default <B> NecoList<B> map(Function<? super A, ? extends B> fn) {
  return lazyCons(fn.apply(head()), () -> map(tail());
}

Wenn wir in diese Richtung gehen wollen, gibt es mehrere Ansätze:

  • in NecoList integrieren (zusätzliche LazyCons-Unterklasse oder Aufbohren von Cons)
  • Schwesterklasse mit gemeinsamen Interface
    Es sollte aber klar sein, dass man dann die üblichen Probleme mit lazy Datenstrukturen hat. Eine naive Implementierung ohne Memoisierung würde z.B. den Supplier bei jedem Durchlauf aufrufen, was Performance-Einbußen, unerwartet mehrfach ausgeführte Seiteneffekte u.s.w. zur Folge hat. Memoisierung benötigt aber zwei Variablen pro Knoten: den Supplier und dessen Ergebnis. Alles machbar (siehe GitHub - bodar/totallylazy: Another functional library for Java ), aber momentan denke ich, dass reverse das kleinere Übel ist.

Was andere mögliche Datenstrukteren angeht: Eine Skiplist würde wohl nicht als immutable Datenstruktur funktionieren (ich lasse mich aber gern eines Besseren belehren). Die einzige mir bekannte Alternative ist die Random Access List (von Okasaki). Die hilft bei diesem Problem auch nicht viel, benötigt deutlich mehr „Buchhaltung“, bietet aber O(log n) Zugriff auf ein Element.

Ein ganz anderes paar Schuhe ist, wenn man eine FIFO-Queue haben will. Das funktioniert mit der „Banker’s Queue“ aus zwei normalen Listen recht einfach (lustigerweise auch mit einigem reverse, aber trotzdem deutlich effizienter als mit einer Liste).

Und dann noch ein… räusper … etwas heikles Thema. rumdrucks. Wie steht ihr zu Dingen wie „JavaDocs“?

Bei einem Release auf jeden Fall, aber solange wir noch am Rumprobieren sind, stört es mehr als es hilft.

Du hast Recht. Mit sortierten Listen funktioniert das nicht, weil ggf. Elemente irgendwo in der Mitte eingefügt werden müssten.

Ich habe mal versuchsweise eine Lazy-Version gepushed (sorry, habe noch nicht alle Tests nachgezogen). Die Implementierung wird dadurch im Allgemeinen einfacher, das Verhalten allerdings weniger vorhersehbar. Z.B. scheitert NecoList.of('a', 'b', 'c', 'd').with(5, 'x'); nicht wie erwartet mit einer IooBE, dazu muss man erst einmal etwas mit der Liste machen oder die neue Methode force() aufrufen (die die Liste einfach nur einmal durchgeht).

Die aktuelle Implementiertung erlaubt “gemischte” Implementierungen, wenn manz.B. LazyCons -> LazyCons -> Nil mit Cons -> Cons -> Nil mit concat verkettet, bekommt man eine LazyCons -> LazyCons -> Cons -> Cons -> Nil - Liste. Gute Performance, aber in bestimmten Fällen “seltsames” Verhalten, wenn man die Laziness nicht beachtet.

Schaut euch mal an, ob wir das prinzipiell so machen wollen. Falls nicht, mache ich den Commit rückgängig.

Dazu habe ich bisher noch keine abschließende Meinung. Das Verhalten vom with finde ich so nicht so prickelnd. Mit einem size in konstanter Zeit könnte man das beheben.
„Seltsames“ Verhalten sollten wir weitestgehend ausschließen. Und wenn das Verhalten dann minimiert ist, brauchen wir noch eine harte Entscheidungsgrundlage: Wie groß ist der Tradeoff aus Speicherverbrauch, Laufzeit und „seltsamem“ Verhalten?
Ich sehe die ganzen Lazy-Sachen erstmal als reine Optimierung, die wir jetzt noch nicht machen sollten, wenngleich ich (explizites) Lazy-Verhalten in einigen Fällen sehr praktisch finde.

*** Edit ***

force ist ein hässlicher Krückstock.

*** Edit ***

Die Knoten könnten so umgesetzt werden:

    A head() throws NoSuchElementException;
    Optional<A> headOpt();
    NecoList<A> tail() throws NoSuchElementException;
    Optional<NecoList<A>> tailOpt();
    A last() throws NoSuchElementException;
    Optional<A> lastOpt();
    NecoList<A> init() throws NoSuchElementException;
    Optional<NecoList<A>> initOpt();
}```

Alles andere kann dann generisch für bspw. `NonLazyNecoList` und `LazyNecoList` implementiert werden. `Node`-Implementierungen wären dann `Cons`, `LazyCons` und `Nil`.

Btw.: [noparse]`[/noparse] ist ein zu langer Tagname ;-)

Ich würde die aktuelle Implementierung nicht so schnell auf den Müll werfen wollen. Was die “Seltsamkeit” angeht: Man muss sich halt immer bewusst sein, dass eine übergebene Liste lazy sein könnte. Wenn man auf referenzielle Transparenz achtet, ist das kein Problem. **Wenn **es ein Problem ist, hat man force(). Die aktuelle Version sollte nicht nur bessere Performance bieten, sondern hat auch eine leichter verständliche Implementierung. Zwei getrennte Typen für lazy und strikte Listen machen die Sache nicht unbedingt besser (was kommt z.B. heraus, wenn man die beiden Listentypen aneinanderhängt?)

OK, ich denke, es ist meiner Meinung nach Zeit, bei den Listen erst einmal einen Schritt zurück zu treten und unabhängig von der aktuellen Implementierung zu überlegen, was wir wirklich wollen.

Ich denke, ein Problem des aktuellen Designs ist, dass es das allgemeine Listen-Interface mit Implementierungsdetails vermengt. Wenn ich mir z.B. die Frage stelle, wie ich eine Random Access List als alternative Implementierung in die aktuelle Struktur einfügen wöllte, habe ich keine klare Antwort.

Ein weiterer Punkt ist die Unterscheidung von strict und lazy-Implemenentierungen. Dabei müssen wir beachten, dass letztere potientiell unendlich lang sein können.

Dann wäre noch wesentlich, ob die Implementierung über Wrapper mit internen Knoten oder direkt (wie jetzt) erfolgen soll.

Auch wenn ich IMMERnoch nicht so konstruktiv/produktiv beitragen konnte, wie das vielleicht erwartet wurde:

Wäre ein akzeptabler Ansatz hier vielleicht, sich mal ganz pragmatisch alle „List“-Interfaces (und deren superinterfaces) aus

http://pcollections.org/

http://functionaljava.org/

anzusehen, Gemeinsamkeiten und Fehlende Punkte zu identifizieren, zu überlegen, was davon man gleich, ähnlich oder ganz anders machen will, was dort noch fehlt (!) und welche Auswirkungen die Java 8 Streams haben werden… ? (Das würde ggf. auch die „Abgrenzung“ gegenüber diesen Projekten klarer machen…)

Ich lege mir bei manchen Beiträgen am Ende ein kleines
[noparse][/noparse] [noparse][/noparse]
[noparse][/noparse] [noparse][/noparse]
-Reservoir an, wo man sich schnell eins rausholen kann :smiley:

Ich bin dabei, ein wenig Interfaces herauszuabstrahieren. Ist nicht ganz einfach, insbesondere wie man mit Laziness umgeht.

Für die Hässlichkeit und Umständlichkeit der with-Methode habe ich einen Lösungsvorschlag: Derartige Operationen überlassen wir vollständig einen Zipper: list.zipper.at(4).replace('x');, analog mit insert, delete u.s.w.

Außerdem ist mir aufgefallen, dass Sortieroperationen noch völlig fehlen. Für einfach verkettete Listen bietet sich Merge-Sort an.

Hast du schon Ergebnisse?

Zum grundsätzlichen Design mit den ganzen default-Methods habe ich heute einen Blogbeitrag gelesen, in dem auf diese Mail von Brian Goetz verlinkt war.

(Abgesehen von diesem einen Zitat ist die Mail äußerst interessant.)

Nachdem ich mich ein wenig mehr mit default-Methods und den Auswirkungen bei deren intensiven Nutzung beschäftigt habe, bin ich für mich zu dem Ergebnis gekommen, dass es vielleicht sinnvoll wäre, einen klassischeren Ansatz zu verwenden und eine Kombination aus Interface + abstrakte Klasse zu entwerfen.
Wie das konkret aussieht ist natürlich auch stark davon abhängig, was deine Abstraktionsansätze bisher ergeben haben bzw. inwiefern wir gemeinsam eine schöne Abstraktion finden.

Ein weiteres Zitat aus der Mail, das mich ein wenig zu den Grundlagen zurückgebracht hat:

Das trifft auf unsere Listen eigentlich haargenau zu.