Wann wird es zu kompliziert, wo "schaltet ihr ab"?

Landei

Top Contributor
Ich habe mich gerade beim Schreiben einer generischen Methode gefragt, wo die Grenze "zumutbaren" Codes liegt, und ab welcher Komplexität man einfach aufgibt und schreiend weg rennt.

Als Diskussionsgrundlage einmal die Methode selbst:

Java:
public static <M,A,N,B,P,C> F2<_<_<µ,M>,A>,_<_<µ,N>,B>,_<_<µ,P>,C>> lift2IdentityT(final F2<_<M,A>,_<N,B>,_<P,C>> fn) {
    return new F2<_<_<µ,M>,A>,_<_<µ,N>,B>,_<_<µ,P>,C>>() {
        @Override
        public _<_<IdentityT.µ, P>, C> $(_<_<IdentityT.µ, M>, A> nestedA, _<_<IdentityT.µ, N>, B> nestedB) {
            IdentityT<M,A> identityA = IdentityT.narrow(nestedA);
            IdentityT<N,B> identityB = IdentityT.narrow(nestedB);
            return new IdentityT<P,C>(fn.$(identityA.get(), identityB.get()));
        }
    };
}

Im Moment, in dem ich das schreibe, ist mir völlig klar, was das tut, dass das auch korrekt ist (hier: weil ich das Original aus Haskell kenne), und weil ich auch mit meinen (nicht ganz standardkonformen, aber wohlbegründeten) Namenskonventionen vertraut bin. Andererseits würde ich in fremden Code mächtig stutzen. Dummerweise lässt sich das Ding momentan nicht sinnvoll vereinfachen (Closures würden etwas helfen), es hat halt einfach 6 generische Parameter.

Was denkt ihr?
 

Tobse

Top Contributor
Also hier würde ich zunächst mal gerne die Definition von [c]_[/c] sehen und dann die Generics übersichtlicher hinschreiben, damit ichs besser erkenne:

Java:
public static <M,A,N,B,P,C> F2<
        _<
            _<µ,M>,
        A>,
        _<
            _<µ,N>,
        B>,
        _<
	    _<µ,P>,
        C>
    > lift2IdentityT(final F2<
        _<M,A>,
        _<N,B>,
        _<P,C>
    > fn) {
// usw...

Aber von aufgeben kann (bei mir) hier noch nicht die rede sein :D
 

ThreadPool

Bekanntes Mitglied
[...] wo die Grenze "zumutbaren" Codes liegt, und ab welcher Komplexität man einfach aufgibt und schreiend weg rennt [...]
Was denkt ihr?

Dein gezeigter Code bzw. ähnliche Experimente (z.B. diese "Typklassen") sind dem Durchschnitt nicht zumutbar, zumindest nicht in der Java-Syntax. Da die allermeisten weder Haskell-Cracks noch irgendwie tiefgreifender mit funktionaler Programmierung vertraut sind und so wie du nicht mal die Korrektheit des Codes überprüfen können.

Und allgemein gesprochen hängt die Fähigkeit zum Verständnis ja auch von der geistigen Kapazität des Lesenden ab. Einem mit IQ von 130 werden wohl einige Dinge schneller eingängig sein als dem Durchschnitt der dafür länger, bis ewig lange, benötigen wird, je nach Problemstellung.
 

ARadauer

Top Contributor
Man kann generische Typen auch schöne sprechende Namen geben... bei mathematischen Problemstellungen ist es aber halt oft der Name A oder M
 

Landei

Top Contributor
Also hier würde ich zunächst mal gerne die Definition von [c]_[/c] sehen

Das ist einfach eine Klasse der Art:

Java:
public class _<M,A> {
     public _(M m) {
         if (m == null) throw new IllegalArgumentException();
     } 
}

Die Grundidee ist folgende Konstruktion für Unterklassen:

Java:
public class List<A> extends _<List.StatischeInnereKlasse,A> {
   public static StatischeInnereKlasse {
      private StatischeInnereKlasse(){
      }
   }
   ....
}

Damit bin ich sicher, dass ein
Code:
_<List.StatischeInnereKlasse,A>
immer ein
Code:
List<A>
sein muss, weil nur innerhalb von
Code:
List
ein
Code:
List.StatischeInnereKlasse
erzeugt werden kann.
Code:
StatischeInnereKlasse
kürze ich überall mit
Code:
µ
ab. Der Vorteil des Konstrukts
Code:
 _<M.µ,A>
ist, dass jetzt der den eigentliche Basistyp
Code:
M
über seinen "Stellvertreter"
Code:
µ
wie ein Typparameter behandelt werden kann, was die Simulation von Typpolymorphismus höherer Ordnung erlaubt. Beachte, dass der Mechanismus in der alten Google-Code-Version etwas anders ist.

Aber das alles nur zum Verständnis, meine Frage bezog sich generell auf Komplexität, meine Bibliothek war hier nur ein geeignetes Beispiel (weil es den verrücktesten Code enthält, den ich bisher geschrieben habe).
 
G

Guest2

Gast
Moin,

Ich habe mich gerade beim Schreiben einer generischen Methode gefragt, wo die Grenze "zumutbaren" Codes liegt, und ab welcher Komplexität man einfach aufgibt und schreiend weg rennt.

Ich würde nicht schreiend wegrennen, wenn aber

1. Sich die (ungefähre) Semantik nicht innerhalb von ~3 Sekunden aus der Methodensignatur ergibt

oder

2. Die Funktionsweise der Methode nicht innerhalb von ~15 Sekunden aus dem Methodenrumpf offensichtlich wird

Dann gehe ich davon aus, dass der Autor die Verwendung der Methode durch Dritte nicht vorgesehen hat.

Das ganze clean Code "Geraffel" ist ja auch nicht zum Selbstzweck entstanden, sondern hatte imho den Zweck Quellcode zu fördern, der schnell und einfach zu erfassen ist (unter anderem). Dein Methodenbeispiel ist zwar schön kurz aber alles andere als einfach zu erfassen.


Dummerweise lässt sich das Ding momentan nicht sinnvoll vereinfachen (Closures würden etwas helfen), es hat halt einfach 6 generische Parameter.

Warum sollte man auch? Die Methode gehört doch zum Lösungskonzept einer funktionalen Programmiersprache. Warum sollte man das so in Java formulieren können sollen?

Viele Grüße,
Fancy
 
G

Gast2

Gast
Moin,

Man kann generische Typen auch schöne sprechende Namen geben
jetzt weis ich aber auch was mich stört :autsch:

Ich habe mich gerade beim Schreiben einer generischen Methode gefragt, wo die Grenze "zumutbaren" Codes liegt,
Gib doch bitte Deinen Kollegen (in welcher Abhängigkeit auch immer) doch wenigstens die Möglichkeit halbwegs etwas zu verstehen. D.h. Namen keine kryptischen Abkürzungen (z.B. µ) und gib der Methodensignatur evt. Kommentare dazwischen (void /* blablub */ method()) oder eben zusätzliche Zeilenumbrüche. Wobei letztere Beiden eher der Lesbarkeit statt der Konvention folgt.

hand, mogel
 

Landei

Top Contributor
Das ist nicht für meine Kollegen, das kommt in eine Bibliothek, und dort ist das generelle Konzept beschrieben. Die einzelnen Klassen haben oft direkte Haskell-Äquivalente. Ihr könnt also ruhig davon ausgehen, dass klar ist, wofür eine Methode gedacht ist.

Längere, sprechende Typparameter machen das ganze noch unübersichtlicher, es ist Absicht, dass [c]_[/c] und [c]µ[/c] optisch nicht besonders in Gewicht fallen (weil sie auch für das generelle Verständnis hier nicht wichtig, sondern eher ein notwendiges Übel zur Konstruktion sind). Aber wie gesagt, es geht mir um die allgemeine Frage, wie weit man gehen sollte, wo die Komplexitätsgrenze ist, nicht um die Feinheiten dieser speziellen Bibliothek.
 
S

SlaterB

Gast
welchen Zweck _ und µ haben, habe ich noch nicht aus den Erklärungen verstanden,
dass es dafür keine einfachere Version a la Enum gibt kann man aber wohl annehmen

dieses Beispiel ist allerdings als Diskussionsgrundlage ungünstig, denn dessen Hauptprobleme für alle sind eben die vielen _ und µ,
die du selber kennst und alle die das Programm lesen sollen sicher auch schon oft genug begegnet sind,
hier aber unbekannt

bleibt die Frage an sich ohne Beispiel, also recht unspezifisch ;)
 

Landei

Top Contributor
welchen Zweck _ und µ haben, habe ich noch nicht aus den Erklärungen verstanden

Das Konzept ist eigentlich ganz einfach. Das hier ist in Java nicht erlaubt:

Java:
interface Functor<X<?>> {
   public <A,B> X<B> map(F1<A,B> function, X<A> value);
}

Es wäre aber schön, wenn es erlaubt wäre, denn dann könnte man ganz abstrakt das Konzept der "Transformation mittels einer Funktion" für einen Datentyp (z.B. X = List, Set...) abbilden. Hat man sein List aber definiert als ...

Java:
class _<X,A> {
   ...
}

class List<A> extends _<List.µ,A> {
   static class µ {
   }
   ...
}

... kann man obiges Interface ausdrücken und auch implementieren:

Java:
interface Functor<M> {
   public <A,B> _<M,B> map(F1<A,B> function, _<M,A> value);
}

class ListFunctor implements Functor<List.µ> {
   public <A,B> _<List.µ,B> map(F1<A,B> function, _<List.µ,B,A> value) {
       ...
   }
}

Ein Nachteil ist natürlich, dass man ein _<List.µ,B> zurückbekommt, aber wir wissen ja (dank einiger Konstruktionsdetails), dass das nur ein List<B> sein kann. Und da sich die Unterstrich-Klassen in den Typklassen recht problemlos ineinanderschachteln lassen und erst am Ende wieder in einen "vernünftigen" Typ umgewandelt werden müssen, ist der Aufwand tatsächlich in einigen Fällen gerechtfertigt. Es ist jedenfalls der einzige Weg, den ich kenne, um einen Großteil der Haskell-Strukturen einigermaßen korrekt und typsicher abzubilden.

Mehr steckt nicht dahinter.
 
Zuletzt bearbeitet:

Marco13

Top Contributor
Es kommt auch darauf an, wer damit arbeitet, und zu welchem Zweck. Ich habe auch schon gelegentlich Strukturen für potentiellen "Produktivcode" anskizziert, der für jemanden, der sich nicht den ganzen Tag damit beschäftigt, nicht "zumutbar" erscheinen konnten. (Und das wohl auch taten, der Code hat seinen Weg nicht ins Produktivsystem gefunden - aber in einem privaten Projekt bastle ich jetzt daran weiter).

An sich gewöhnt man sich ja an derartige Konstrukte. Ähnlich wie in der Mathematik: Dort sind Dinge nicht "schwer" oder "einfach", sie SIND - nicht mehr, und nicht weniger. Funktionale Elemente liegen eben an der Grenze dessen, was mit Java überhaupt darstellbar ist - und ob es nun eine "vernünftige", "sinnvolle", oder in diesem Fall "zumutbare" Darstellung ist, liegt im Auge des Betrachters. Schon bestimmte Funktionen von FunctionalJava, z.B. sowas hier, könnten, wie man so schön sagt "scare the sh!t out of" jemanden, der schon mit dem Generics-Parameter einer List<T> überfordert ist.

In diesem konkreten Beispiel kommt noch etwas dazu, was es "weniger zumutbar" macht, als es sein müßte: Das _ und das µ. Den Namen _ habe ich bisher nur verwendet, wenn ich code im Stil vom The International Obfuscated C Code Contest schreiben wollte. Aber ich finde es im Hinblick darauf, dass es "nicht auffällig sein soll" fast noch besser, als den Fauxpas von FunctionalJava, eine Klasse "F" zu nennen, und deswegen bei den Typparameternamen auf "A,B,C,D,E,F$,G,H" ausweichen zu müssen. Über das µ könnte man noch eher streiten. Ich mußte kurz auf der Tastatur suchen...

Ein weiterer Punkt, der es schwierig macht, sind die Multi-Level-Generics. Die sind eben immer etwas unübersichtlich. Eine Einrückung wie von Tobse vorgeschlagen könnte helfen, aber in der vorgeschlagenen Form IMHO doch nicht so viel.

Außerdem ist es bei Generics praktisch immer so, dass die Methodendeklaration total :autsch: aussieht, aber sie so aussehen muss, damit man sie leicht verwenden kann. Und vermutlich wäre das ganze auf einmal nicht mehr so abschreckend, wenn man es in einem Beispiel sehen würde.


Indirekt zum eigentlichen Thema, aber ganz konkret bezogen auf den geposteten Code: Ich wundere mich, dass du ohne Bounds auskommst (oder auszukommen glaubst....? Gerade DU solltest dir da eigentlich sicher sein, deswegen wundert mich das so ???:L ). Zumindest habe ich, konkret beim oben angedeuteten Projekt, schon bei so etwas vermeintlich trivialem (und wie gesagt: "Schön einfach aussehenden") wie
Java:
Iterator<Number> iterator = Iteration.iteratorOverIterables(Arrays.asList(floatList, integerList));
in der Methodendeklaration dann sowas wie
Java:
    public static <S extends Iterable<? extends T>, T> Iterator<T> 
        iteratorOverIterables(Iterable<S> iterablesIterable)
    {
        ...
    }
stehen, damit das eben auch funktioniert, wenn man mit einer List<Float> und einer List<Integer> einen Iterator<Number> hinterfüttern können will. Ich kann mir kaum vorstellen, wie die ganzen Bounds-Anforderungen bei funktionalen Elementen (noch) höherer Ordnung einfach überflüssig werden... :bahnhof:
 

Landei

Top Contributor
Bounds benötige ich überhaupt nicht (Wildcards dagegen schon). Der Grund ist, dass etwa eine Liste kein Funktor ist, sondern es einen separaten Funktor für Listen gibt (ganz nach Haskell-Stil, wo die Typklassen "neben" den Datentypen stehen). Wenn ich irgendwo einen Klasse brauche, die einen Funktor besitzt, muss ich diesen mitliefern (in Haskell wäre das eine Context Bound, in Scala ein implizites Objekt, und in Java würde Guice helfen) und in einem gewissen Sinne ist dieses Extra-Argument meine "Bound".
 

Marco13

Top Contributor
Der Grund ist, dass etwa eine Liste kein Funktor ist, sondern es einen separaten Funktor für Listen gibt (ganz nach Haskell-Stil, wo die Typklassen "neben" den Datentypen stehen). Wenn ich irgendwo einen Klasse brauche, die einen Funktor besitzt, muss ich diesen mitliefern

Um also die Frage im Titel zu beantworten: HIER :D

Zumindest fast. Du wolltest sowas wie
Java:
interface Functor<X<?>> {
   public <A,B> X<B> map(F1<A,B> function, X<A> value);
}
Weil man damit so schöne Sachen machen könnte wie
Java:
List<Integer> list = functor.map(functionForParsingIntegerToString, stringList);

(ich kenne "Functor" in leicht anderer Bedeutung - von C++ her eher als das, was wir in Java als "anonyme Funktion", oder besser "implementierung eines Function-Interfaces" bezeichnen würden, aber sei das mal egal).


Könntest du (ohne ZU oft zu betonen, wie "einfach" das ganze doch ist) in dem Code mit den vielen "..." mal einige der "..." durch etwas suggestiveres ersetzen...?
Java:
class _<X,A> {
   ...
}
 
class List<A> extends _<List.µ,A> {
   static class µ {
       // Ist das eine Art Factory?
   }
   ...
}

interface Functor<M> {
   public <A,B> _<M,B> map(F1<A,B> function, _<M,A> value);
}
 
class ListFunctor implements Functor<List.µ> {
   // Warum hat das letzte _ drei Typparameter? Oder sollte das nur _<List.µ,A> sein?
   public <A,B> _<List.µ,B> map(F1<A,B> function, _<List.µ,B,A> value) {
       // Hier steht irgendwas vollkommen generisches!?
   }
}


Falls ich das richtig verstanden habe, und versuchen sollte, es (noch ;) ) einfacher auszudrücken: Es geht darum, dass man mit dem Typ "ArrayList<String>" in vielen Fällen nicht das (in der Allgemeingültigkeit) anfangen kann, was man will. Deswegen soll das auf etwas wie "Platzhalter<ArrayList, String>" hochgezogen werden? Ist es auch richtig, dass der eigentliche Knackpunkt die Erzeugung von Objekten (sprich Rückgabewerten) ist?
 

Landei

Top Contributor
Falls ich das richtig verstanden habe, und versuchen sollte, es (noch ;) ) einfacher auszudrücken: Es geht darum, dass man mit dem Typ "ArrayList<String>" in vielen Fällen nicht das (in der Allgemeingültigkeit) anfangen kann, was man will. Deswegen soll das auf etwas wie "Platzhalter<ArrayList, String>" hochgezogen werden?

Exakt, nur kann man es leider nicht so machen, weil ArrayList ja selbst einen Typparameter hat, man würde also bei [c]Platzhalter<ArrayList<?>, String>[/c] landen, was einen nicht weiterbringt. Hier kommt µ ins Spiel, das einfach ein Stellvertreter für die Klasse ArrayList "an sich" ist.

Ist es auch richtig, dass der eigentliche Knackpunkt die Erzeugung von Objekten (sprich Rückgabewerten) ist?

Ja, hier sind Casts notwendig (meistens auch bei der Implementierung von Methoden wie map). Allerdings kann man den in einer statischen Methode verstecken, und den Compiler mit SuppressWarnings besänftigen.

Ich hatte mich bei dem Beispiel übrigens vertippt. Hier mal eine Implementierung (java.util.List ist natürlich nicht möglich, da es nicht von _ erbt, aber hier sind immutable, einfach verlinkte Listen sowieso besser geeignet).

Java:
class ListFunctor implements Functor<List.µ> {
   public <A,B> _<List.µ,B> map(F1<A,B> function, _<List.µ,A> value) {
      List<A> listA = (List) value; // den Cast kann man noch in eine Methode verpacken
      List<B> result = new List<B>(); // also NIL
      for(A a : listA) {
         result = new List<B>(function.apply(a), result); //apply ist bei mir $, analog zum Haskell-Operator
      }
      return result.reverse();  //hier ist kein Cast nötig, List<B> ist ja ein _<List.µ, B>  
   }
}
 
Zuletzt bearbeitet:

Ähnliche Java Themen


Oben