Java-Kontrollstrukturen 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, 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:

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:

public static <A> A if2(boolean choice, 
    Supplier<A> ifTrue, 
    Supplier<A> 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:

public static <A> Function<Boolean, A> if2Fun(
    Supplier<A> ifTrue, 
    Supplier<A> ifFalse) {
    return b -> if2(b, ifTrue, ifFalse);
}

//Anwendung
Function<Boolean, String> 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:

public class If3<A> {

    private Optional<A> result = Optional.empty();

    public static <A> If3<A> if3(boolean choice, 
        Supplier<A> ifTrue) {
        If3<A> if3 = new If3<>();
        if (choice) {
            if3.result = Optional.of(ifTrue.get());
        }
        return if3;
    }

    public If3<A> elseIf3(boolean choice, 
        Supplier<A> ifTrue) {
        if (! result.isPresent() && choice) {
           result = Optional.of(ifTrue.get());
        }
        return this;
    }

    public A else3(Supplier<A> 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:

public class If4<T,A> {

    private Map<Predicate<T>, Supplier<A>> cases = 
        new LinkedHashMap<>();

    public static <T,A> If4<T,A> if4(
        Predicate<T> choice, 
        Supplier<A> ifTrue) {
        If4<T,A> if4 = new If4<>();
        if4.cases.put(choice, ifTrue);
        return if4;
    }

    public If4<T,A> elseIf4(Predicate<T> choice, 
        Supplier<A> ifTrue) {
        cases.put(choice, ifTrue);
        return this;
    }

    public Function<T, A> else4(Supplier<A> ifTrue) {
        return t -> {
            for(Map.Entry<Predicate<T>, Supplier<A>> entry : 
                cases.entrySet()) {
                if (entry.getKey().test(t)) {
                    return entry.getValue().get();
                }
            }
            return ifTrue.get();
        };
    }
}

//Anwendung
Function<Integer, String> 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.

Advertisements

Kommentar verfassen

Trage deine Daten unten ein oder klicke ein Icon um dich einzuloggen:

WordPress.com-Logo

Du kommentierst mit Deinem WordPress.com-Konto. Abmelden / Ändern )

Twitter-Bild

Du kommentierst mit Deinem Twitter-Konto. Abmelden / Ändern )

Facebook-Foto

Du kommentierst mit Deinem Facebook-Konto. Abmelden / Ändern )

Google+ Foto

Du kommentierst mit Deinem Google+-Konto. Abmelden / Ändern )

Verbinde mit %s