Java now has switch expressions. The cond does not have a pattern matching language, but tests anything, rather thann a value.

Expressive Conditionals in Java - Reclaiming the 'if' expression

The if statement is the common conditional construct for making decisions. With the advent of Java 8 and lambda expressions, it has become much easier to define new conditional constructs. I’ll show you a few ways to make Java low level decisions more expressive.

The if statement does not represent a value, which means that unless it is used to purely decide on side effects, you either have to duplicate code or use mutating variables. Java has the ternary operator (?:) that is an expression that represents a value, but may be found difficult to read. With lambdas, it’s easy to create an if-expression that returns a value.

<T> T ifelse(boolean test, Supplier<T> then, Supplier<T> otherwise) {
   return test ? then.get() : otherwise.get();
}

It’s just a wrapper around the ternary operator that puts the intention, that it is an if-else expression, first. It makes it easier to read. The expression represents a value.

String result = ifelse(true, () -> "yes", () -> "no");

The if statement is like a Swiss army knife, used for many types of conditionals. It does not reveal the type. Are we producing a value, are we doing a side effect, is there an else case, or are there many cases? ifelse tells us up front that there is an else case, so we do not have to search for it.

doWhen

The doWhen tells us do: that it is a side effect without a value and when: that there is no else condition.

doWhen(tooWarm, Thermostat::decreaseTemperature);
void doWhen (boolean test, Runnable then){
   if(test)
      then.run();
   }

unless - exceptional if

Then there are exceptional if statements, where there is a normal flow, but under some circumstances something exceptional may have occurred. Checking the exceptional condition is not the important part of the program flow, and under such circumstances it’s easier to read the important program flow first:

// make the string lower unless it's null
String lowerOrNothing(String a){
   return Choices.unless(() -> a.toLowerCase(), a==null, ()->"Nothing")
}
<T> T unless(Supplier<T> then, boolean test, Supplier<T> otherwise) {
   if (test)
      return otherwise.get();
   return then.get();
}

either - if with a flow

Sometimes you want the test value flow through the expression, without needing to have to mention it.

String upperOrLowerOnFirst(String str){
   return either(
      str,
      s -> s.matches("\\p{Upper}.*"),
      String::toUpperCase,
      String::toLowerCase);
}
public static <T,V> V either(
      T t,
      Predicate<? super T> p,
      Function<? super T,? extends V> then,
      Function<? super T,? extends V> otherwise) {
   return ifelse(
      p.test(t),
      () -> then.apply(t),
      () -> otherwise.apply(t));
}

cond

Then there is the situation when there are many cases to consider, that the switch-case statement is meant for. However there are mostly many things to consider so we kind of resort to complex usage if the if statement, checking all sorts of things while making a decision, making the decision logic harder to follow. Probably with nested if statements with many temporary variables.

The Lisp programming language used the cond expression long before the C-like syntax was invented. It is more powerful, as it represents a value, can have any number of conditions, and can check anything. It’s like a cleaner version of nested if statements. And in Clojure, the lisp of today, a cond expression like:

(cond 
   (= 2 (count "Three")) (count "Four") 
   (< 3 4) (+ 1 2)
   :else 5)  ;; :else is a keyword. Keywords are neither false or nil, meaning they are truth

… which is 3, is a macro that expands to nested if expressions:

(if (= 2 (count "Three"))
 (count "Four")
 (if (< 3 4) 
   (+ 1 2) 
   (if :else 
     5 
     nil)))

The cond expression has pairs of expressions, a predicate and a corresponding expression. The cond expression returns the value of the first successful test. Unless it continues with the next condition, and short-circuits on success. This is powerful because you can test multiple conditions in order, and still return a single value — much like the ternary operator, but more flexible.

A similar cond expression in Java could look like:


int three = cond (() -> 2 == "Three".length(), () -> "Four".length(),
                  () -> 3 < 4, ()-> 1 + 2,
                  () -> 5);

This cond can easily be implemented with pairs of predicate and value Suppliers, like:

  /**
   * Evaluates each test t in order until true, in which case value of corresponding v is returned.
   * When all values are false, or null, returns the value of last v.
   * Evaluations short-circuits on truth. There are multiple overloads supporting different arities.
   * <p>Example:
   * <pre>{@code
   * cond(() -> false, () -> "No",
   *      () -> true, () -> "Yes",
   *      () -> "catch all")  => "Already caught"
   * }</pre>
   */
public static <T> T cond(
      Supplier<Boolean> t0, Supplier<? extends T> v0,
      Supplier<Boolean> t1, Supplier<? extends T> v1,
      Supplier<? extends T> r){
   requireAllNonNull(t0, v0, t1, v1, r);
   return ifelse(truth(t0.get()),
         v0::get,
         ()->cond(t1, v1, r));
}

// Where cond with next arity is called up on first test failure 

public static <T> T cond(
      Supplier<Boolean> t0, Supplier<? extends T> v0,
      Supplier<? extends T> r){
   requireAllNonNull(t0,v0,r);
   return ifelse(truth(t0.get()), v0::get, r::get);
}

boolean truth(Boolean b){
   return Objects.nonNull(b) && b;
}

This gives us a conditional expression just as powerful as Lisp’s cond, but in plain Java. The Suppliers are used to prevent evaluation before the choice has been made, and to short circuit the expression.

condPred

It might be a bit easier to read when each case is written using predicates and values directly, rather than explicit lambdas for everything.

 int result = Choices.condPred(
            this::is3InLength, "Three", this::return4, 
            x -> isLessThan(4, x), 3, ()-> 3,
            () -> 5
        );

…which is 3.

The condPred can be implemented with similar code, where the predicate is a Predicate and the value is supplied as is.

public static <T, T0, T1> T condPred(
         Predicate<T0> t0, T0 v0, Supplier<? extends T> s0,
         Predicate<T1> t1, T1 v1, Supplier<? extends T> s1,
         Supplier<? extends T> r) {
   requireAllNonNull(t0, s0, t1, s1, r);
   return ifelse(truth(t0.test(v0)),
            castSupplier(s0),
            () -> condPred( t1, v1, s1, r));
}

//Where condPred with next arity is called up on first test failure 

public static <T, U> T condPred(
         Predicate<U> t0, U u, Supplier<? extends T> v0,
         Supplier<? extends T> v1) {
   requireAllNonNull(t0, v0, v1);
   return ifelse(truth(t0.test(u)), v0::get, v1::get);
}

@SuppressWarnings("unchecked")
private static <T> Supplier<T> castSupplier(Supplier<? extends T> s) {
   return (Supplier<T>) s;
}

condFlow

It’s also possible to have a cond variant where the tested value flows through to the result expression — similar to either

public static <T, U, A> T condFlow(
         Predicate<? super U> test0,
         Supplier<? extends U> value0,
         Function<? super U, ? extends T> result0,
         Predicate<? super A> test1,
         Supplier<? extends A> value1,
         Function<? super A, ? extends T> result1,
         Supplier<? extends T> otherwise) {
   requireAllNonNull(test0, value0, result0,
            test1, value1, result1,otherwise);
   var value = value0.get();
   return ifelse(truth(test0.test(value)),
            () -> result0.apply(value),
            () -> condFlow(test1, value1, result1, otherwise));
}

// Which calls

public static <T, U, A> T condFlow(
         Predicate<? super U> test0,
         Supplier<? extends U> value0,
         Function<? super U, ? extends T> result0,
         Supplier<? extends T> otherwise) {
   requireAllNonNull(test0, value0, result0, otherwise);
   var value = value0.get();
   return ifelse(truth(test0.test(value)),
            () -> result0.apply(value),
            otherwise::get);
}

Where the supplied value is passed to a function, here toString and the Hello lambda when test succeeds

String result = condFlow(
                alwaysFalse(), constInt(1), Integer::toString(),
                x -> true, () -> 2), x -> "Hello " + x,
                () -> "fallback");

Supplier<Integer> constInt(int val) {
   return () -> val;
}

static <T> Predicate<T> alwaysFalse() {
   return x -> false;
}

Note how we defer value evaluation using suppliers, so it only happens if no earlier test succeeds. There are a lot of variations of how a if statement can be replaced with conditional constructs that tells the story up front. These constructs make Java more expressive — not by changing the language, but by embracing what it already provides. They strip away boilerplate, clarify intent, and tell a story — just like all good code should.


All #art #clojure #csharp #data-structures #database #datomic #emacs #functional #haskell #history #immutability #java #jit #jmm #lambdas #lisp #pioneers #poetry #programming #programming-philosophy #randomness #rant #reducers #repl #smalltalk #sql #threads #women