Totpunkt

Wir scheinen uns momentan an einem gewissen Totpunkt zu befinden, aber ich finde die Sachen, die wir schon haben, nicht schlecht. Ich hoffe, ihr habt nicht die Lust verloren. Ich will jedenfalls weitermachen.

Collections sind schwierig (ich habe mir z.B. http://www.youtube.com/watch?v=uiJycy6dFSQ angesehen, wo Paul Phillips mit Scalas Collections ins Gericht geht), und es bringt nichts, blind voranzustürmen (z.B. war mein Vorschlag, alles in ein map-artiges Framework zu packen, nicht unbedingt schlecht, aber in unserem Rahmen wohl zu exotisch, und ich werde ihn erst einmal nicht weiter verfolgen). Stattdessen sollten wir versuchen, uns auf das Wesentliche zu konzentrieren und jede Featuritis vermeiden.

Wie soll es nun weitergehen? Ich sehe zwei Hauptprobleme: Wie gehen wir mit Laziness/Strictness um, und wie abstrahieren wir unsere Collections?

Meine Meinung ist, dass immutable Collections ohne Laziness wenig nützlich sind, und dass das einfach der Standardfall werden sollte, zumal man ja leicht eine lazy Collection zu einer strikten machen kann. Ich weiß, dass das zu unerwarteten Ergebnissen führen kann, aber wenn alles, was an einer solchen Stelle nötig ist, ein “force”-Aufruf ist, finde ich das akzeptabel. Falls jemand eine bessere Idee hat, wie man Laziness und Strictness “nebeneinander” ausdrücken kann, bin ich dafür offen. Aber ich habe länger darüber nachgedacht und mir ist keine elegante Implementierung eingefallen.

Nun gibt es mehrer Arten, wie man Laziness implementieren kann. Ich favorisiere memoisierende Laziness (also wenn ein Element einmal ausgewertet wurde, ist es “bekannt” und findet keine erneute Berechnung statt). Als “Testballon” arbeite ich an einer Stream-Klasse (im funktionales Sinn, also eine Art unendliche einfach verkettete Liste), die ich zur Diskussion stellen möchte.

Ich denke, es ist auch für das Finden geeigneter Abstraktionen wichtig, nicht immer nur um NecoList zu kreisen, sondern auch andere Klassen als Beispiel zur Verfügung zu haben. Ich denke erst so werden einige Probleme der API deutlich (was ist z.B. mit size(), last(), init()?) Neben Stream würde ich eventuell noch einen einfachen immutablen Array-Wrapper schreiben, und vielleicht noch ein Set, oder hat von euch jemand Lust? Dann können wir anhand konkreter Beispiele über Abstraktionen diskutieren, ohne uns in Spekulationen zu verlieren.

Außerdem würde ich vorschlagen, perspektivisch doch auf GitHub umzuziehen. Es ist dumm, auf Feedback zu verzichten, nur weil Leute den Code nicht sehen können. Ich weiß, dass man Hemmungen hat, “unfertigen” Code zu zeigen, aber von außen wirkt das wie Geheimniskrämerei.

Mal als kurze Sicht von außen: Der kleine Absatz summiert alles, was ich über das Projekt bisher dachte. „Hm, mal gucken, was die haben… oh, closed, dann halt nicht.“ Nie wieder drüber nachgedacht. Wenn das mit einem passiert ist, der auch etwas beizutragen gehabt hätte, ist euch jener Input durch die Lappen gegangen.

Und zum Thema unfertiger Code: https://www.youtube.com/watch?v=0SARbwvhupQ

TotPUNKT… nunja, inzwischen wäre eine Aussage wie „Wir hatten vor ein paar Monaten einen Lebe-Punkt“ schon fast angebrachter :smiley: :frowning:

Ein Hemmschuh für mich ist - neben mangelnder Erfahrung und Versiertheit mit bestimmten Themen (Optional kapier’ ich noch, aber schon bei Either tauchen die ersten Fragezeichen auf) - die Gewissheit, dass schon viele schlaue Leute viel Arbeit in solche Themen gesteckt haben. Die (schon diskutierte) Frage nach einer Abgrenzung gegenüber bestehenden Bibliotheken ist eine Sache. Schon das allein wäre eine ziemliche Mammutaufgabe. (Aber das „todo“, das ich mal angedeutet hatte - nämlich bei all den anderen Libraries zu schauen, wie denn die Verarbungshierarchien bzw. (List)-Interfaces aussehen - steht immer noch auf meiner Liste). Dazu kommen aber Libs, die schon Teile von etablierten Spachen sind. Bei Scala und Clojure (und ggf. Groovy) sind schon Implementierungen dabei, bei denen ich mir (zumindest bei oberflächlicher Betrachtung) nur denken konnte: „WTF!?“. Also, interfaces zu designen und sich zu überlegen, wie die alle schön ineinandergreifen könnten, ist eine Sache. Aber eine gute Implementierung ist dann nochmal was anderes. (Und die Frage, ob man gute/sinnvolle interfaces designen kann, ohne nicht mindestens zwei gute alternative Implementierungen für diese Interfaces im Hinterkopf zu haben, sei mal dahingestelt).

(Nebnebei: Ich fand den Gedanken der Map-basierten Collections übrigens besonders interessant. Es wird sicher schwierig, das „konsistent“ und „stimmig“ zu machen, aber die Tatsache, dass java.util.Map keine java.util.Collection ist, hat mich immer schon ein bißchen gestört. Und noch nebenbeier: Das ganze mit Streams zu verbinden wäre sicher sinnvoll, aber auch da muss ich mich erst noch näher durch Spliterator & Co wühlen, um zu verstehen, was da abläuft…)

EDIT: Vielleicht noch ein Nachtrag zum Thema „Öffentlichkeit“ etc (schau’ gerade das Video). Ich persönlich tue mich da sehr schwer damit. Ich entwickle praktisch ausschließlich allein, Code den niemand sieht. Wenn ich mal irgendwas öffentlich mache, und da dann im JavaDoc eines private fields (ja!) ein Tippfehler ist, ist das so :eek: Ich weiß, dass an vielen Stellen (vor allem bei einem Multi-Millarden-Dollar-Konzern, der sein Geld mit Code verdient) der Leitsatz „Release Early, Release Often“ propagiert wird, aber … die Vorstellung, dass andere über mich das denken, was ich über Leute denke, die jeden zusammengestümperten Quälkot, den sie je in ihrem Leben fabriziert haben, auf ein öffentliches Repo hochladen, verursacht ein gewisses Unbehagen. http://de.wikipedia.org/wiki/Carl_Friedrich_Gauß#Arbeitsweise_von_Gau.C3.9F

Stimmt :slight_smile:

Ein Hemmschuh für mich ist - neben mangelnder Erfahrung und Versiertheit mit bestimmten Themen (Optional kapier’ ich noch, aber schon bei Either tauchen die ersten Fragezeichen auf) - die Gewissheit, dass schon viele schlaue Leute viel Arbeit in solche Themen gesteckt haben. Die (schon diskutierte) Frage nach einer Abgrenzung gegenüber bestehenden Bibliotheken ist eine Sache. Schon das allein wäre eine ziemliche Mammutaufgabe. (Aber das „todo“, das ich mal angedeutet hatte - nämlich bei all den anderen Libraries zu schauen, wie denn die Verarbungshierarchien bzw. (List)-Interfaces aussehen - steht immer noch auf meiner Liste). Dazu kommen aber Libs, die schon Teile von etablierten Spachen sind. Bei Scala und Clojure (und ggf. Groovy) sind schon Implementierungen dabei, bei denen ich mir (zumindest bei oberflächlicher Betrachtung) nur denken konnte: „WTF!?“. Also, interfaces zu designen und sich zu überlegen, wie die alle schön ineinandergreifen könnten, ist eine Sache. Aber eine gute Implementierung ist dann nochmal was anderes. (Und die Frage, ob man gute/sinnvolle interfaces designen kann, ohne nicht mindestens zwei gute alternative Implementierungen für diese Interfaces im Hinterkopf zu haben, sei mal dahingestelt).

Manchmal ist es auch wirklich WTF!?! (siehe z.B. das oben erwähnte Video), manches ist overengineered. Meine Ausgangspunkt war, dass man eigentlich in fast allen Fällen mit sehr wenigen Collections auskommt - sagen wir z.B. ArrayList, HashSet und HashMap. Das Hauptproblem mit diesen Klassen ist für mich aber nicht die (durchaus verbesserungswürdige) API, sondern dass sie veränderlich (und strict) sind. Die immutablen Gegenstücke, ein dünnes, aber korrektes Abstraktionslevel darüber sowie ein paar Konvertierungsfunktionen würden eine enorme Erleicherung darstellen. Dazu noch ein paar Klassen, die sowieso fehlen (auch zur Implementierung der Collections, siehe z.B. die Map.Entry-Krücke) und gut is…

(Nebnebei: Ich fand den Gedanken der Map-basierten Collections übrigens besonders interessant. Es wird sicher schwierig, das „konsistent“ und „stimmig“ zu machen, aber die Tatsache, dass java.util.Map keine java.util.Collection ist, hat mich immer schon ein bißchen gestört.

Da würde ich mich erst dranwagen, nachdem ich wenigstens eine halbwegs vernünftige „normale“ Implementierung vorweisen kann.

Und noch nebenbeier: Das ganze mit Streams zu verbinden wäre sicher sinnvoll, aber auch da muss ich mich erst noch näher durch Spliterator & Co wühlen, um zu verstehen, was da abläuft…)

Ganz ehrlich, dazu habe ich auch keine richtige Lust. Die Stream-Klasse, von der ich spreche, ist einfach eine ganz normale einfach verkettete Liste, nur ohne abschließendes Nil - was natürlich nur funktionieren kann, wenn der jeweilige „Schwanz“ lazy berechnet werden kann. Java Streams (und Iteratoren) haben dazu durchaus Ähnlichkeiten, aber eben auch Unterschiede. Diese Namenskollision ist bedauerlich, aber funktionale Sprachen verwenden den Begriff „Stream“ im genannten Sinne schon länger als es Java überhaupt gibt.

Hier der aktuelle Code zu Stream - keine Ahnung, ob er funktioniert, und es fehlen wichtige Methoden. Ein solcher Stream ist immer unendlich, kann also keine vernünftige Implementierung von size(), init() oder last() besitzen.

import java.util.Iterator;
import java.util.function.Function;
import java.util.function.Predicate;
import java.util.function.Supplier;

public class Stream<A> implements Iterable<A> {

    private Memo<A> head;
    private Memo<Stream<A>> tail;

    public Stream(A head, Stream<A> tail) {
        this.head = new Evaluated<>(head);
        this.tail = new Evaluated<>(tail);
    }

    public Stream(A head, Supplier<Stream<A>> tailSupplier) {
        this.head = new Evaluated<>(head);
        this.tail = new UnEvaluated<>(tailSupplier);
    }

    public Stream(Supplier<A> headSupplier, Stream<A> tail) {
        this.head = new UnEvaluated<>(headSupplier);
        this.tail = new Evaluated<>(tail);
    }

    public Stream(Supplier<A> headSupplier, Supplier<Stream<A>> tailSupplier) {
        this.head = new UnEvaluated<>(headSupplier);
        this.tail = new UnEvaluated<>(tailSupplier);
    }

    public A head() {
        Evaluated<A> evaluated = head.evaluate();
        head = evaluated;
        return evaluated.get();
    }

    public Stream<A> tail() {
        Evaluated<Stream<A>> evaluated = tail.evaluate();
        tail = evaluated;
        return evaluated.get();
    }

    @Override
    public Iterator<A> iterator() {
        return new Iterator<A>() {

            private Stream<A> stream = Stream.this;

            @Override
            public boolean hasNext() {
                return true;
            }

            @Override
            public A next() {
                A result = stream.head();
                stream = stream.tail();
                return result;
            }
        };
    }

    public <B> Stream<B> map(Function<? super A, ? extends B> fn) {
        return new Stream<B>(() -> fn.apply(head()), () -> tail().map(fn));
    }

    public Stream<A> dropWhile(Predicate<? super A> predicate) {
        return dropUntil(predicate.negate());
    }

    public Stream<A> dropUntil(Predicate<? super A> predicate) {
        for(Stream<A> stream = this; ; stream = stream.tail()) {
            if (predicate.test(stream.head())) {
                return stream;
            }
        }
    }

    public Stream<A> filter(Predicate<? super A> predicate) {
        Stream<A> stream = this.dropUntil(predicate);
        return new Stream<>(stream.head(), () -> stream.tail().filter(predicate));
    }

    private static interface Memo<T> {
        Evaluated<T> evaluate();
    }

    private static class Evaluated<T> implements Memo<T> {
        private final T t;

        private Evaluated(T t) {
            this.t = t;
        }

        @Override
        public Evaluated<T> evaluate() {
            return this;
        }

        public T get() {
            return t;
        }
    }

    private static class UnEvaluated<T> implements Memo<T> {

        private final Supplier<T> supplier;

        private UnEvaluated(Supplier<T> supplier) {
            this.supplier = supplier;
        }

        @Override
        public Evaluated<T> evaluate() {
            return new Evaluated<>(supplier.get());
        }
    }
}

Das sieht komplizierter aus, als es ist, weil sowohl head wie auch tail direkt oder als Supplier übergeben werden können, und zusätzlich jeder Supplier höchstens einmal ausgeführt werden soll (Memoisierung).

EDIT: Vielleicht noch ein Nachtrag zum Thema „Öffentlichkeit“ etc (schau’ gerade das Video). Ich persönlich tue mich da sehr schwer damit. Ich entwickle praktisch ausschließlich allein, Code den niemand sieht. Wenn ich mal irgendwas öffentlich mache, und da dann im JavaDoc eines private fields (ja!) ein Tippfehler ist, ist das so :eek: Ich weiß, dass an vielen Stellen (vor allem bei einem Multi-Millarden-Dollar-Konzern, der sein Geld mit Code verdient) der Leitsatz „Release Early, Release Often“ propagiert wird, aber … die Vorstellung, dass andere über mich das denken, was ich über Leute denke, die jeden zusammengestümperten Quälkot, den sie je in ihrem Leben fabriziert haben, auf ein öffentliches Repo hochladen, verursacht ein gewisses Unbehagen. Carl Friedrich Gauß – Wikipedia

Ich bin da relativ schmerzfrei: Wer es besser kann, soll es besser machen.

Zum Thema „Öffentlichkeit“, ist eigentlich nur Angst vor vor Angst vor Ablehnung, verschwindet nach ein paar mal der Konfrontation damit, danach freut man sich fast schon auf Kritik :slight_smile:

Ansonsten:
Mir ist das zu hoch,vielleicht könnte ich das irgendwann nutzen, aber zum mitentwickeln bin ich wohl nicht gut genug.
Macht weiter! :slight_smile:

In Bezug auf öffentlichen Code habe ich auch nicht so die Probleme. Meinetwegen gerne.
Und zu “Experimentalcode” wie den oben zum Stream: mach doch einfach einen Branch auf. Den können wir doch gerne wieder wegwerfen, wenn der Ansatz so nicht funktioniert.
Ich finde es zumindest immer angenehmer, wenn ich den Code einfach auschecken kann und dann alles direkt in der IDE sehe. So kopiere ich das auch manuell in die IDE und sehe es mir erst dann an. Im Board bekomme ich irgendwie nie den rechten Überblick über Code, der mehr als 10-20 Zeilen hat.

Habe die Stream-Diskussion abgespaltet: http://forum.byte-welt.net/threads/13185-Funktionale-Stream-Klasse