Errors Without Exceptions

Functional Programming in Java

Created by Mark Perry, @mprry, G+, Blog, LinkedIn, GitHub, maperry78@yahoo.com.au

Referential Transparency

  • Replace terms with value

    // append not RT
    List<Integer> list1 = new LinkedList<>();
    list1.append(1);
    List<Integer> list2 = list1; // [1]

    List<Integer> list1 = new LinkedList<>();
    list1.append(1);
    List<Integer> list2 = new LinkedList<>(); // []

Exceptions break RT


// always throw exception
int func(int i) {
    int x = raise();
    try {
        return x + 1;
    } catch (Exception e) {
        return 0;
    }
}

int raise() throws Exception {
    throw new Exception("fail!");
}

// substitution for x changes meaning
int func(int i) {
    try {
        return raise() + 1;
    } catch (Exception e) {
        return 0;
    }
}

Exceptions break RT (2)


int func() {
    try {
        return g(throw new Exception1(), throw new Exception2());
    } catch (Exception1 e) {
        return 1;
    } catch (Exception2 e) {
        return 2;
    }
}

int func() {
    try {
        int a = f1(1);
        int b = f2(2);
        int c = a + f3(3);
        return f4(a, b, c);
    } catch (Exception1 e) {
        return 1;
    } catch (Exception2 e) {
        return 2;
    } (catch Exception3 e)
        return 3;
    }
}

Exceptions and RT

  • RT breaks when handling exceptions
  • Worse when combined with:
    • Large call stack
    • Mutable Global State
    • Threading, Futures and Promises
  • Uses implicit stack state
  • Modelled using continuations

Exception Alternatives

  • Use a sentinel value
    • Errors propagate silently
    • No valid sentinel?
    • Callers must know of special values, bad for reuse (HOF)
  • Force caller to give default value
    • Immediate callers must have default
    • Need to defer decision

Option Data Type

  • Need object that sometimes has a value
  • Convert functions from partial to total

class Option<A>
class Some<A> extends Option<A>
class None<A> extends Option<A>

Option<Double> mean(List<Double> list) {
    return list.isEmpty() ? none() :
        some(list.fold((acc, d) -> acc + d, 0.0) / list.size());
}

Optional Functions

  • Examples: Map lookup, first/last in list
  • Factor out common error handling

class Option<A> {
    static <A> Option<A> none();
    static <A> Option<A> some(A a);

    boolean isNone();
    boolean isSome();

    A orSome(A a);
    Option<A> orElse(Option<A> oa);
    Option<B> map(F<A, B> f);
    Option<B> bind(F<A, Option<B>> f); // aka flatMap, >>=
    Option<A> filter(F<A, Boolean> f);
    Option<C> liftM2(Option<B> o, F2<A, B, C>); // personal favourite
}

Example


@Value.Immutable
interface Employee {
    String name();
    String department();
}
Map<String, Employee> employeesByName = createMap();

String joeDept = employeesByName.get("Joe")
    .map(e -> e.department())
    .filter(d -> d.equals("Accounting"))
    .orSome("Default Dept");

public String getJoeDept() {
    String defaultDept = "Default Dept";
    String key = "Joe";
    if (!employeesByName.hasKey(key)) {
        return defaultDept;
    } else {
        Department dept = employeesByName.get(key).getDepartment();
        return dept.equals("Accounting") ? dept : defaultDept;
    }
}

Composition and Lifting

  • Does Option pollute entire codebase?
  • Map lifts ordinary functions into Option
  • Turns A -> B into Option<A> -> Option<B>

F<Option<A>, Option<B>> lift(F<A, B> f) {
    return oa -> oa.map(f);
}

Lifting Using Map


import java.util.regex.*;
Option<Pattern> pattern(String s) {
    try {
        return Option.some(Pattern.compile(s));
    } catch (PatternSyntaxException e) {
        return Option.none();
    }
}

Option<Boolean> doesMatch(String pattern, String s) {
    return pattern(pattern).map(p -> p.matcher(s));
}

Option<F<String, Boolean>> mkMatcher(String pat) {
    return pattern(pat).map(p -> (s -> p.matcher(s).matches()));
}

Lifting Two Arguments


Option<Boolean> bothMatch(String pat1, String pat2, String s) {
    return doesMatch(pat1, s).bind(b1 -> doesMatch(pat2, s).map(b2 -> b1 && b2));
}

Generalise


Option<C> liftM2(Option<A> oa, Option<B> ob, F2<A, B, C> f)

Boolean bothMatch(String p1, String p2, String s) {
    return liftM2(doesMatch(p1, s), doesMatch(p2, s),
        (b1, b2) -> b1 && b2)
    ).orSome(false);
}

Exercises


    static <A, B> Option<B> map(Option<A> oa, F<A, B> f)
    static <A> Option<A> filter(Option<A> oa, F<A, Boolean> f)
    static <A, B> Option<B> bind(Option<A> oa, F<A, Option<B>> f)
    static <A, B, C> Option<C> liftM2(Option<A> oa, Option<B> ob, F2<A, B, C> f)
    static <A> Option<List<A>> sequence(List<Option<A>> list)
    static <A, B> Option<List<B>> traverse(List<A> list, F<A, Option<B>> f)

Validation

  • Option too simplistic
  • Validation gives a reason for failure
  • Disjoint union of two types

abstract class Validation<E, A> {
    static <E, A> Validation<E, A> fail(E e);
    static <E, A> Validation<E, A> success(A a);
}
class Failure<E, A> extends Validation<E, A>;
class Success<E, A> extends Validation<E, A>;

Creating Validation


Validation<Exception, Double> safeDiv(Double num, Double denom) {
    try {
        return Validation.success(x / y);
    } catch (Exception e) {
        return Validation.failure(e);
    }
}

Validation Functions


    <B> Validation<E, B> map(F<A, B> f);
    <B> Validation<E, B> bind(F<A, Validation<E, B>> f);
    Validation<E, A> orElse(Validation<E, A> v);
    A orSuccess(A a);
    <B, C> Validation<E, C> liftM2(Validation<E, B> v, F2<A, B, C> f);
}

Validation Example


interface Person {
    Name name();
    Age age();
}
interface Name {
    String value();
}
interface Age {
    int value();
}

class PersonBuilder {
    Validation<String, Name> createName(name: String) {
        return isNullOrEmpty(name) ? Validation.fail("Name is empty.") :
            Validation.success(new Name(name));
    }
    Validation<String, Age> createAge(int age) {
        return age < 0 ? fail("Age is out of range.") : success(new Age(age));
    }
    Validation<String, Person> createPerson(String name, int age) {
        return createName(name).liftM2(createAge(age), (n, a) -> new Person(n, a));
    }
}

Exercises



<B> Validation<E, B> map(F<A, B> f);
<B> Validation<E, B> bind(F<A, Validation<E, B>> f);
Validation<E, A> orElse(Validation<E, A> v);
A orSuccess(A a);
<B, C> Validation<E, C> liftM2(Validation<E, B> v, F2<A, B, C> f);

static <E, A> Validation<E, List<A>> sequence(List<Validation<E, A>>)
static <E, A, B> Validation<E, List<B>> traverse(List<A> list, F<A, Validation<E, B>> f)

Conclusion

  • Option and Validation are:
    • modular
    • compositional
    • simple to reason about
  • Reuse functions that manipulate errors

Afterword

  • Functional Programming in Scala, Chiusano and Bjarnason
  • Chapter 4, Handling Errors Without Exceptions

Created by Mark Perry, @mprry, G+, Blog, LinkedIn, GitHub, maperry78@yahoo.com.au