Java-Kontrollstukturen nachgebaut - if

Wieder so ein seltsamer Beitrag: Wozu sollte man Kontrollstrukturen nachbauen? Nun, selbst als reine Fingerübung lernt man einiges über die Möglichkeiten - und Unmöglichkeiten - der Sprache, aber die eigentliche Idee ist, einen soliden Ausgangspunkt für eigene Erweiterungen zu schaffen.

Beginnen wir mit einer der einfachsten Kontrollstrukturen, nämlich if - else, und dem verwandten ternären Operator x ? y : z. Das klassische if ist leicht zu modellieren - die einzige Schwierigkeit ist, dass wir schön „lazy“ bleiben, also einen Zweig nur dann ausführen, wenn die Entscheidungsvariable das auch vorsieht:

  Code:

public static void if1(boolean choice, Runnable ifTrue, Runnable ifFalse) {
if (choice) {
ifTrue.run();
} else {
ifFalse.run();
}
}

/Anwendung
if1(System.currentTimeMillis() % 2 == 0,
() → System.out.println(„ifTrue“),
() → System.out.println(„ifFalse“));
Allerdings wäre solcher Code in der funktionalen Programmierung verpönt, denn er tut nur eines: Seiteneffekte ausführen. Viel nützlicher wäre ein Verhalten, wie es der ternäre Operator zeigt, der stattdessen einen Wert zurückliefert. Nichts leichter als das:

  Code:

public static A if2(boolean choice, Supplier ifTrue, Supplier ifFalse) {
return choice ? ifTrue.get() : ifFalse.get();
}

//Anwendung
String s = if2(System.currentTimeMillis() % 2 == 0, () → „trueValue“, () → „falseValue“);
Wir haben jetzt schon einen kleinen Vorteil gegenüber dem ternären Operator: Die beiden Zweige können aus mehreren Operationen bestehen. Trotzdem geht es noch besser. Was ist z.B., wenn man das Konstrukt wiederverwenden will? Nun, mit einer kleine Änderung können wir die Auswertung „auf später“ verschieben, und damit eine Mehrfachnutzung erleichtern:

  Code:

public static Function if2Fun(Supplier ifTrue, Supplier ifFalse) {
return b → if2(b, ifTrue, ifFalse);
}

//Anwendung
Function f = if2Fun(() → „trueValue“, () → „falseValue“);
System.out.println(f.apply(System.currentTimeMillis() % 2 == 0));
System.out.println(f.apply(System.currentTimeMillis() % 2 == 0));
Ein anderer Schwachpunkt ist, dass sich unsere bisherigen Konstrukte schlecht schachteln lassen. Sicher sind ellenlange if-else-Kaskaden nicht die feine englische Art, aber hin und wieder lassen sie sich doch nicht vermeiden. Jetzt brauchen wir schon Fluent Interfaces, und bewegen uns langsam in Richtung DSL:

  Code:

public class If3 {

private Optional result = Optional.empty();
public static  If3 if3(boolean choice, Supplier ifTrue) {
    If3 if3 = new If3();
    if (choice) {
        if3.result = Optional.of(ifTrue.get());
    }
    return if3;
}
public If3 elseIf3(boolean choice, Supplier ifTrue) {
    if (! result.isPresent() && choice) {
       result = Optional.of(ifTrue.get());
    }
    return this;
}
public A else3(Supplier ifTrue) {
    return result.orElseGet(ifTrue);
}

}

//Anwendung
int i = 15;
String s = if3 (i < 10, () → „kleiner 10“)
.elseIf3 (i < 100, () → „kleiner 100“)
.else3(() → „größer gleich 100“);
Es ist auch möglich, die Variante, die eine Funktion liefert, und die letzte „kaskadierende“ Variante zu kombinieren, unter der Voraussetzung, dass alle Tests auf einen einzigen Wert ausgeführt werden können:

  Code:

public class If4 {

private Map

, Supplier> cases = new LinkedHashMap();

public static  If4 if4(Predicate choice, Supplier ifTrue) {
    If4 if4 = new If4();
    if4.cases.put(choice, ifTrue);
    return if4;
}
public If4 elseIf4(Predicate choice, Supplier ifTrue) {
    cases.put(choice, ifTrue);
    return this;
}
public Function else4(Supplier ifTrue) {
    return t -> {
        for(Map.Entry

, Supplier> entry : cases.entrySet()) {
if (entry.getKey().test(t)) {
return entry.getValue().get();
}
}
return ifTrue.get();
};
}
}

//Anwendung
Function f = if4((Integer i) → i < 10, () → „kleiner 10“)
.elseIf4(i → i < 100, () → „kleiner 100“)
.else4(() → „größer gleich 100“);
System.out.println(f.apply(15));
Allerdings sieht man hier, dass damit die Grenzen von Javas Typinferenz-Mechanismus erreicht sind: Das Prädikat in der if4-Methode benötigt die Typangabe (Integer i), um zu kompilieren. Trotzdem ist es interessant, was alles möglich und - fast noch wichtiger - auch praktikabel ist.

Ich hoffe, die Zeit zu finden, diese kleine Serie fortzusetzen. Neben den offensichtlichen Kandidaten wie case und for dürfte auch try mit allen seinen Varianten (inklusive ARM) interessant sein.

(Dieser Beitrag findet sich auch auf meinem Blog: Java-Kontrollstrukturen nachgebaut – if – eSCALAtion Blog )

Weiterlesen…