Strictness and Laziness

Functional Programming in Java

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

Motivation

  • Previously used map, fold, filter and bind over Lists
  • Composition over lists inefficient
  • 
    List.list(1, 2, 3, 4)
        .map(i -> i + 10)
        .filter(i -> i % 2 == 0)
        .map(i -> i  * 3);
    // List.list(36,42)
    						
  • Inefficient memory usage - intermediate states
  • Build better List
  • Laziness improves modularity

Lazy Values

interface F0<A> {
    A f();
}
abstract class P1<A> implements F0<A> {
    abstract A _1();
    A f() {...}
    P1<B> map(F<A, B> f) {...}
    P1<B> bind(F<A, P1<B>> f) {...}
    P1<A> join(P1<P1<A>> p) {...}
    P1<C> liftM2(P1<B> p, F<A, B, C> f) {...}
    // more methods
}
abstract class P2<A, B> {
    abstract A _1();
    abstract B _2();
    // more methods
}

Creating Values

public static P1<A> p(A a) {
    return new P1<A>() {
        A _1() {
            return a;
        }
    };
}
P1<Integer> p1 = P.p(1);

public static P1<A> lazy(F0<A> f) {
    return new P1<A>() {
        A _1() {
            return f.f();
        }
    };
}
P1<Integer> p2 = P.lazy(() -> intFunction());

Lazy Boolean Operations

  • Familiar: and, or

static boolean or(boolean b, F0<Boolean> f) {
    return b ? true : f.f();
}
or(true, () -> false);
or(true, () -> expensiveMethod());

static boolean and(boolean b, F0<Boolean> f) {
    return b ? f.f() : false;
}
and(false, () -> expensiveMethod());

					

Lazy If


static A if2(boolean b, F0<A> onTrue, F0<A> onFalse) {
    return b ? onTrue.f() : onFalse.f();
}

if2(false, () -> expensiveMethod(), () -> 3);
					

Cached Lazy Values

  • If no effects then can cache result
  • memoisation

P1<Integer> p = P.lazy(() -> factorial(100)).memo();
int a = p._1(); // very slow
int b = p._1(); // return calculated value
					

Streams

  • A lazy list
  • Eager head, lazy tail
  • Composed functions on streams use single pass

abstract class Stream<A> {
    abstract A head();
    P1<Stream<A>> tail();

    static Stream<A> stream(A... as) { ... }
    static Stream<A> cons(A head, F0<Stream<A>> tail) { ... }

    <B> Stream<B> map(F<A, B> f) { ... }
    Stream<A> filter(F<A, Boolean> f) { ... }
    <B> B fold(F2<B, A, B> f, B b) { ... }
    <B> Stream<B> bind(F<A, Stream<B>> f) { ... }
}
					

Exercises


static <A> List<A> toList(Stream<A> s);
static <A> Stream<A> take(Stream<A> s, int n);
static <A> Stream<A> takeWhile(Stream<A> s, F<A, Boolean> f);
					

Separation of Concerns

  • Laziness separates description from evaluation
  • Describe large expression, evaluate part

Short Circuit Example


static <A> boolean exists(Stream<A> s, F<A, Boolean>) {
    if (s.isEmpty()) {
        return false;
    } else {
        return f.f(s.head()) ? true : exists(s.tail()._1(), f);
    }
}
					

Lazy Fold



static <A, B> B foldRight(Stream<A> s, F0<B> acc, F2<F0<B>, A, B> f) {
    if (s.isEmpty()) {
        return acc.f();
    } else {
        return f.f(() -> foldRight(s.tail()._1(), acc, f), s.head());
    }
}

static <A> boolean exists(Stream<A> s, F<A, Boolean> f) {
    return foldRight(s, () -> false, (lb, a) -> f.f(a) || lb.f());
}
					
  • A lazy accumulator
  • If the head satisfies the function then return
  • Reuse foldRight due to laziness
  • Strict foldRight requires early termination
  • Laziness gives more reuse!

Exercise

  • Use foldRight to implement forAll, takeWhile
  • Implement map, filter, append and bind using foldRight

Example


F<Integer, Integer> add = i -> i + 10;
F<Integer, Boolean> even = i -> i % 2 == 0;
stream(1, 2, 3, 4).map(add).filter(even).toList();

Stream.cons(11, stream(2, 3, 4).map(add)).filter(even).toList();
Stream.stream(2, 3, 4).map(add).filter(even).toList();
Stream.cons(12, stream(3, 4).map(add)).filter(even).toList();
List.cons(12, stream(3, 4).map(add).filter(even)).toList();
List.cons(12, Stream.cons(13, stream(4).map(add)).filter(even).toList());
List.cons(12, stream(4).map(add).filter(even).toList());
List.cons(12, Stream.cons(14, Stream.nil().map(add)).filter(even).toList());
List.cons(12, List.cons(14, Stream.nil().map(add).filter(even).toList()));
List.cons(12, List.cons(14, List.nil());

					
  • Process an element at a time
  • Filter and map interleaved
  • No intermediate answers, memory efficient
  • Streams as first-class loops

Infinite Streams

  • Can create infinite streams because tail is lazy
  • Only process finite portion

Stream<Integer> s1 = Stream.range(1); // [1, 2, 3, ...]
List<Integer> list = s1.take(5).toList()
Stream<String> s2 = Stream.repeat("a"); // ["a", "a", "a", ...]
					
Write
// assuming existence of cons
static <A> Stream<A> cons(A head, F0<Stream<A>> tail);

static <A> Stream<A> repeat(A a);
static Stream<Integer> from(int i);
static Stream<Integer> range(int low, int high); // [low, high)
static Stream<Integer> fibonacci(int a, int b);
static Stream<A> unfold(S s, F<S, Option<P2<A, S>>> f);
					

Unfold

  • Unfold is co-recursive - what?
  • Recursion consumes data and terminates
  • Co-recursion produces data and is productive (co-terminates)
  • Can always evaluate more in finite time

Summary

  • Laziness is efficient and modular
  • Separate description of infinite expression and evaluation
  • Reuse description in different contexts
  • Functions can evaluate different portion of infinite stream

Afterword

  • Functional Programming in Scala, Chiusano and Bjarnason
  • Chapter 5, Strictness and Laziness

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