DSLs: Operatoren als Methoden-Argumente?

Nehmen wir mal als Beispiel eine ganz einfache DSL für Vergleichsoperationen (meiner Meinung nach eine der misslungensten Java-APIs).

Welchen Stil würdet ihr vorziehen, und warum? Oder würdet ihr es ganz anders machen?

1) Einzelne Argumente, Operatoren als Methoden

boolean b1 = is(3).lessThan(5);
boolean b2 = is(3).greaterOrEqual(3);
boolean b3 = using(Comparator.<String>reverseOrder()).is("z").lessThan("a");

Implementierung:
[SPOILER]

public final class Ord<T> {

    private final T firstValue;
    private final Comparator<? super T> comparator;

    private Ord(T firstValue, Comparator<? super T> comparator) {
        this.firstValue = firstValue;
        this.comparator = comparator;
    }

    static <T extends Comparable<? super T>> Ord<T> is(T firstValue) {
        return new Ord<>(firstValue, Comparator.naturalOrder());
    }

    interface Using<T> {
        Ord<T> is(T firstValue);
    }

    static <T> Using<T> using(Comparator<? super T> comparator) {
        return value -> new Ord<>(value, comparator);
    }

    public boolean equalTo(T secondValue) {
        return comparator.compare(firstValue,secondValue) == 0;
    }

    public boolean lessThan(T secondValue) {
        return comparator.compare(firstValue,secondValue) < 0;
    }

    public boolean lessOrEqual(T secondValue) {
        return comparator.compare(firstValue,secondValue) <= 0;
    }

    public boolean greaterThan(T secondValue) {
        return comparator.compare(firstValue,secondValue) > 0;
    }

    public boolean greaterOrEqual(T secondValue) {
        return comparator.compare(firstValue,secondValue) >= 0;
    }
}

[/SPOILER]

2) Mehrere Argumente inklusive Operatoren

boolean b1 = is(3, lessThan, 5);
boolean b2 = is(3, greaterOrEqual, 3);
boolean b3 = using(Comparator.<String>reverseOrder()).is("z", lessThan, "a");

Implementierung:
[spoiler]

public enum Ord {
    lessThan(i -> i < 0),
    lessOrEqual(i -> i <= 0),
    equalTo(i -> i == 0),
    greaterOrEqual(i -> i >= 0),
    greaterThan(i -> i > 0);

    private final Predicate<Integer> pred;

    Ord(Predicate<Integer> pred) {
        this.pred = pred;
    }

    public static <T extends Comparable<? super T>> boolean is(T firstValue, Ord ord, T secondValue) {
        return ord.pred.test(firstValue.compareTo(secondValue));
    }

    public static <T> Using<T> using(Comparator<? super T> comparator) {
        return (first, ord, second) -> ord.pred.test(comparator.compare(first,second));
    }

    public interface Using<T> {
        boolean is(T firstValue, Ord ord, T secondValue);
    }
}

[/spoiler]

Ganz spontan, durch draufschauen, sieht das erste “natürlichsprachlicher” aus - und allgemeiner. Ob und wie man das zweite geschickt über Integer hinaus verallgemeinern könnte, müßte man sich überlegen.
(Aber auch, ob eins von beidem bei nicht-so-spontanem Draufschauen gravierende technische Nachteile hat)

Einzelne Argumente scheint mir leserlicher. Das andere ist doch absurd, und kann möglicherweise nicht vom Compiler erkannt werden.

Btw. Hint. Offtopic.:
Heißt es nicht
bei Nicht-so-spontanem-Draufschauen (,genauso,) als Kinder-in-die-Welt-Setzer?!
Manchmal sehe ich ein paar “grobe” Schnitzer in der Rechtschreibung, Marco. Sieh’s bitte nicht als negative Kritik, sondern Reaktion einer “Provokation” michbezüglich deinerseits ein Thema zuvor (“wirres Gestammel”?!).

Was bitte soll vom Compiler nicht erkannt werden? Schmeiß doch bitte einfach mal deine IDE an, bevor du wüste Vermutungen um dich wirfst…

Programmierfehler seitens des Users/Anwenders/Programmierers meinte ich damit. Z. B. wenn sie/er lesThan anstatt lessThan schreibt. Kann das vom Compiler möglicherweise nicht erkannt werden.

Keep cool. :confused:

Natürlich kann es das, lessThan ist eine Enum-Instanz. Nochmal, du solltest wirklich versuchen Code zu verstehen, bevor du ihn kritisierst.

Keep cool. :confused:

Bin ich, wirklich. Ich kann nur so was nicht einfach stehen lassen, dass ist meine OCD.

ich finde auch die erste besser. Mehrere Argumente verwirrt eher meiner Ansicht nach. Bei erster Lösung kann man auch dem User direkt zeigen, welche Möglichkeiten hat.

abgesehen scheint sie besser erweiterbar zu sein, wenn man z.b.

is(3).lessThan(5).biggerThan(1).equal(3)

bauen will.

Ich hab ihn gelesen (wenn auch erst hinterher).

„Eigentlich“ würde man die Operatoren mit Strings „“ realisieren. Was hätte man denn vor Java 1.6 oder 1.5 gemacht?

Dann hatte ich halt eine falsche Annahme.

Und falsche Strings als Args können erst spät erkannt werden, falsche Methodenbezeichner gar kein Problem.

Wenn der „math. Term“ sehr kompliziert ist, würd ich Methodik 1 - wie gesagt - vorziehen, weil sie für mich - und andere anscheinend - übersichtlicher erscheint.

Just my 2 cents, hth

Ich verstehe zwar was du damit erreichen möchtest, persönlich möchte ich mit sowas aber nicht arbeiten, das wäre mir zu viel Tipparbeit ohne viel Zugewinn. Des Weiteren fehlen zur Vollständigkeit noch min/max-Operationen. Als Ergebnis gibt es nur Wahrheitswerte zurück, für logische Operationen falle ich aus der „DSL“ raus und greife auf eingebaute Dinge zurück. Ich sehe da keinen Vorteil im Gegensatz zu einer „Ord“-Klasse als Namespace die mir die benötigte Funktionalität als statische Funktionen mit zwei oder mehr Argumenten zur Verfügung stellt und alternativ einen Satz an Funktionen die mir Predicates zurückliefern die logisch kombiniert werden können.

PS: Es gibt ein IntPredicate, so dass man keine Integers boxen muss.

Netter Versuch, dich rauszureden, aber leider vergeblich:

Davon abgesehen, dass das schon ziemlich prähistorisch ist (*), hat es auch damals schon Möglichkeiten gegeben, sich etwas wie ein Enum zu bauen. Ein entsprechendes Pattern wurde in „Effective Java“ von Josh Bloch beschrieben, und es gab z.B. auch Unterstützung durch Apache Commons: Enum (Commons Lang 2.6 API)

Der Grund, Enums als Sprach-Feature einzuführen, waren neben Bequemlichkeit hauptsächlich Probleme beim Serialisieren (ohne Compiler-Unterstützung konnte man beim Deserialisieren doppelte Pseudo-Enum-Instanzen nicht vermeiden), und das spielt hier keine Rolle.

Wie dem auch sei, auch ein Prä-Java-5-Enum würde an dieser Stelle funktionieren, und einfach einen String zu verwenden, wäre schon damals unnötig und unsauber gewesen.

(*) was impliziert, dass ich auch prähistorisch bin, denn ich war dabei. Ein Grund mehr vorsichtig zu sein, mit mir über Schnee von gestern zu diskutieren - ich könnte mich erinnern. Jedenfalls solange ich noch kein Alzheimer habe…

*** Edit ***

[QUOTE=bygones;133781]ich finde auch die erste besser. Mehrere Argumente verwirrt eher meiner Ansicht nach. Bei erster Lösung kann man auch dem User direkt zeigen, welche Möglichkeiten hat.

abgesehen scheint sie besser erweiterbar zu sein, wenn man z.b.

is(3).lessThan(5).biggerThan(1).equal(3)

bauen will.[/QUOTE]

Das würde aber nur mit einem abschließenden Call funktionieren, der das Fluent Interface in einen Boolean umwandelt, also

is(3).lessThan(5).biggerThan(1).equal(3).get()

oder so…

@ThreadPool Mir ging es hier mehr um den Stil als um das konkrete Beispiel (wobei ich immer dreimal hingucken muss, wie das int-Ergebnis von compare oder compareTo zu interpretieren ist). Natürlich müsste eine „ordentliche“ DSL für Vergleiche eine Menge mehr bieten.

boolean b3 = using(Comparator.<String>reverseOrder()).is("z").lessThan("a");

boolean b3 = using(Comparator.<String>reverseOrder()).is("z", lessThan, "a");

hier sollte in allen Varianten etwas Richtung

boolean b3 = usingReverse().is("z").lessThan("a");

boolean b3 = usingReverse().is("z", lessThan, "a");

oder noch kürzer möglich sein,
usingReverse() schränkt dabei noch nicht auf String ein, wobei das hinsichtlich Comparator.reverseOrder() eh recht unspezifisch ist,

usingReverse() an sich würde freilich nicht-typisiertes Objekt (welcher Klasse auch immer) liefern, etwas unschön,
aber da sonst die Typisierung erst bei static-is() anfängt auch nicht viel anders

notfalls usingReverse() oder so :wink:


auf Comparator.reverseOrder() könnte dann generell noch verzichtet werden,
wenn schon eigene Klasse für Vergleiche, dann kann man auch selber das compare-Ergebnis je nach Flag umkehren

die internen Methoden

        return comparator.compare(firstValue,secondValue) == 0;
    }
 
    public boolean lessThan(T secondValue) {
        return comparator.compare(firstValue,secondValue) < 0;
    }
 
    public boolean lessOrEqual(T secondValue) {
        return comparator.compare(firstValue,secondValue) <= 0;
    }

sollten sowieso auf etwa

        return compareTo(secondValue) == 0;
    }
 
    public boolean lessThan(T secondValue) {
        return compareTo(secondValue) < 0;
    }
 
    public boolean lessOrEqual(T secondValue) {
        return compareTo(secondValue) <= 0;
    }

geändert werden, nur eine zentrale Methode für Vergleiche, Zugriff auf Comparator + firstValue, an dieser Stelle dann auch Negation bei Flag,
aber der Code hier ist hier natürlich sicher nur kleine Testversion

Vorteil des Verzichts auf Comparator.reverseOrder() wäre jedenfalls noch einfachere Übergabe eines anderen spezifischen Comparators,
der dann auch zu reversen ist, wobei es langfristig wahrscheinlich eh möglich sein sollte, mehrere Comparatoren kombinieren zu können

und Comparator.reversed() gibt es allgemein ja auch noch, viele Wege nach Bielefeld

Ja, verstehe ich, aber was macht deine Umsetzung besser als eine Menge von statischen Funktionen die genauso heißen und zwei Argumente übernehmen? Du hast jetzt nur ein Predicate reingeschoben, welchen Zugewinn bringt das?

Na ja, Lesbarkeit - das ist schließlich eine der Hauptaufgaben einer DSL. In beiden Fällen hat man ja (ohne Punktuierung) „is 3 lessThan 5“ und nicht „isLessThan 3, 5“. Eine Sprache wie Scala würde auch noch erlauben, das „is“ loszuwerden.

die Variante mit den Enum ist ja viel mächtiger, erlaubt das Kriterium als Variable,
für mich dann keine Stil-Frage mehr sondern eine Einsatz-Frage,
wenn man die Einsatz-Möglichkeit möchte dann Stil unterzuordnen

mit den Enums bestände zudem die Möglichkeit, den Code etwas umzustellen:
boolean b1 = lessThan.is(3, 5);
wobei das wieder der Lesbarkeit abträglich sein könnte wie schon erwähnt

eine nette Möglichkeit ergäbe sich aber jedenfalls noch beim reverse() oder negate():
greaterOrEqual.negate().is(3, 5)
schon nicht mehr lesbar, aber technisch hübsch, jedes Enum könnte genau das passende andere Enum liefern, hier etwa lessOrEqual, sonst nichts weiter zu tun

da wäre nicht mehr unschön statische Methode is + Using-Objekt auch mit gleicher is-Methode nötig,
allerdings auch nur hinsichtlich reserve(), für andere Comparatoren doch wieder

na, vielleicht auch schon alles selbst durchdacht und alter Hut :wink:

[QUOTE=SlaterB]die Variante mit den Enum ist ja viel mächtiger, erlaubt das Kriterium als Variable,
für mich dann keine Stil-Frage mehr sondern eine Einsatz-Frage,
wenn man die Einsatz-Möglichkeit möchte dann Stil unterzuordnen
[/QUOTE]

Das ist einer der Punkte, die ich unter

[QUOTE=Marco13;133764]
(Aber auch, ob eins von beidem bei nicht-so-spontanem Draufschauen gravierende technische Nachteile hat)[/QUOTE]
einordnen würde.

Ganz konkret-suggestives Beispiel: Man zeigt im GUI eine Tabelle an, und der Benutzer kann aus einer JComboBox das Sortierkriterium für eine Spalte wählen. In diesem Fall wäre der Code einfach
boolean b2 = is(x, selectedComparisonFromComboBox, y);

Beim “fluent”-Stil müßte man dann entweder eine if-Kaskade basteln (häßlich), oder irgendwie eine Method Reference für die passende Methode in die JComboBox legen (auch häßlich).

bei Sortierkriterium usw. war ich auch, aber das ist ja eigentlich die Welt der Comparatoren,
hier ein etwas anderes Thema, obwohl auch mit reingebracht,
Einsatz zur Sortierung aber zumindest gar nicht direkt gut möglich

durchaus vorstellbar trotz Enums, dass nie variables Vergleichskriterium eingesetzt werden soll,
aber vorhanden ist die Möglichkeit schon bei der zweiten Variante, ‘zu Risiken und Nebenwirkungen …’

Ich finde ja aus ausfürhbarer Code schon zu tief. Eine DSL wär für mich schon noch abstrakter ^^

Die Fluentvariante liest sich tatsächlich nett, ist aber verboser und muss mit einem initialen “Builder” arbeiten.
Da Du hier binäre Operatoren nachbaust/versteckst, sollten die Methoden dafür meiner Meinung nach auch binäre Operatoren (hier im Sinne der Methodensignatur) sein, also zwei Argumente haben. Nicht umsonst gibt es die diversen BiXXX-Interfaces in der Function-API.

Is das nich bisschen uebertrieben? Normale Menschen nehmen <, >, <= und >= oder halt die compareTo Methode.

Wozu ein API zum Vergleichen von Werten? Oder ist das nur ein Beispiel fuer APIs allgemein?