Skip to content

Towards Declarative Programming

Chen YiJia edited this page Mar 20, 2021 · 2 revisions

The Weird World of Functional Interfaces

Predicate: takes an argument and returns a boolean.

Function: argument and return types differ.

Supplier: takes no argument and returns a value.

Consumer: takes an argument and returns nothing.

Interface Function Signature Example
Predicate<T> boolean test(T t) Collection::isEmpty
Function<T,R> R apply(T t) Arrays::asList
Supplier<T> T get() Math::random
Consumer<T> void accept(T t) System.out::println

Primitive Variations

There are three variants of each of the above interfaces to operate on the primitive types int, long and double. Their names are derived from the basic interfaces by prefixing then with a primitive type.

Examples: IntPredicate, LongFunction<int[]>, DoubleSupplier.

Function Variations

For use when the result type is primitive.

  • If both the sources and result types are primitive, prefix Function with SrcToResult, for example LongToIntFunction.
  • If the source is a primitive and the result is an object reference, prefix Function with <Src>ToObj, for example DoubleToObjFunction.

2-Argument Variations

BiPredicate<T,U>, BiFunction<T,U,R>, and BiConsumer<T,U>.

  • There are also BiFunction variants returning the three relevant primitive types: ToIntBiFunction<T,U>, ToLongBiFunction<T,U> and ToDoubleBiFunction<T,U>.

  • There are two-argument variants of Consumer that take one object reference and one primitive type: ObjDoubleConsumer<T>, ObjIntConsumer<T> and ObjLongConsumer<T>.

Others

BooleanSupplier, a variant of Supplier that returns boolean values.

Streams and Optionals

Definitions

stream: a finite or infinite sequence of data elements

stream pipeline: a multistage computation on these elements

// Suppose we have an arbitrary list of words and we want to count all words that are longer than 10 characters long.
import java.util.List;
List<String> words; // assume declared

// classical iteration
int count = 0;
for (String w: words) {
    if (w.length() > 10) {
        count++;
    }
}

// streams
long count = words.stream().filter(w -> w.length() > 12).count();

Stream Properties

  1. A stream does not store its elements. They may be stored in an underlying collection or generated on demand.
  2. Stream operations don't mutate their source. For example, the filter method does not remove elemtns from a stream but yields a new stream in which they are not present.
  3. Stream operations are lazy when possible. This means they are not exectuted until their result is needed. For example, if you only ask for the first five long words instead of all, the filter method will stop filtering after the fifth match. As a consequence, you can have infinite streams.

Setting up a Stream Pipeline

  1. Create a stream
  2. Specify intermediate operations for transforming the initial stream into others, possibly in multiple steps.
  3. Apply a terminal operation to produce a result. This operation forces the execution of the lazy operations that precede it. Afterwards, the stream can no longer be used.

filter, map and flatMap methods

The argument of filter is a Predicate<T>, a function from T to boolean.

map is used to transform the values in a stream by passing the function that carries out the transformation. For example, you can transform all words to lowercase like this:

Stream<String> lowercaseWords = words.stream().map(String::toLowerCase);

When you use map, a function is applied to each element, and the result is a new stream with the results.

<R> Stream<R> flatMap(Function<? super T,? extends Stream<? extends R>> mapper)

yields a stream obtained by concatenating the results of applying mapper to the elements of this stream. (Note that each result is a stream.)

The Optional Type

Optional<T> represents an immutable container that can hold either a single non-null T reference or nothing at all. An optional that contains nothing is said to be empty. A value is said to be present in an optional that is not empty. An optional is essentially an immutable collection that can hold at most one element.

Let's describe a method to calculate the maximum value in a collection, according to its elements' natural order.

public static <E extends Comparable<E>> E max(Collection<E> c) {
    if (c.isEmpty()) {
        throw new IllegalArgumentException("Empty Collection");
    }
    E result = null;
    for (E e: c) {
        if (result == null || e.compareTo(result) > 0) {
            result = e;
        }
    }
    return result;
}

// Using Optionals
import java.util.Optional;
public static <E extends Comparable<E>> Optional<E> max(Collection<E> c) {
    if (c.isEmpty()) {
        return Optional.empty();
    }
    E result = null;
    for (E e: c) {
        if (result == null || e.compareTo(result) > 0) {
            result = e;
        }
    }
    return Optional.of(result);
}

If a method returns an optional, the client gets to choose what action to take if the method can't return a value.

// You can provide a chosen default value
String lastWord = max(words).orElse("No words!");

// Throw a chosen exception
HighScore highScore = max(highscores).orElseThrow(new NoHighScoreException());

// Get the value from a nonempty optional
Element lastNobleGas = max(Elements.NOBLE_GASES).get();

java.util.Optional

return type method Description
T orElse(T other) yields the value of this Optional, or other if this Optional is empty.
T orElseGet(Supplier<? extends T> other) yields the value of this Optional, or the result of invoking other if this Optional is empty.
<X extends Throwable> T orElseThrow(Supplier<? extends X> exceptionSupplier) yields the value of this Optional, or throws the result of invoking exceptionSupplier if this Optional is empty.
void ifPresent(Consumer<? super T> action) if this Optional is nonempty, passes its value to action.
void ifPresentOrElse(Consumer<? super T> action, Runnable emptyAction) if this Optional is nonempty, passes its value to action, else invokes emptyAction.
<U> Optional<U> map(Function<? super T,? extends U> mapper) yields an Optional whose value is obtained by applying the given function to the value of this Optional if present, or an empty Optional otherwise.
Optional<T> filter(Predicate<? super T> predicate) yields an Optional with the value of this Optional if it fulfills the given predicate, or an empty Optional otherwise.
Optional<T> or(Supplier<? extends Optional<? extends T>> supplier) yields this Optional if it is nonempty, or the one produced by the supplier otherwise.
T get()
T orElseThrow() yields the value of this Optional, or throws a NoSuchElementException if it is empty.
boolean isPresent() returns true if this Optional is not empty.
<T> Optional<T> of(T value)
<T> Optional<T> ofNullable(T value) yields an Optional with the given value. If value is null, the first method throws a NullPointerException and the second method yields an empty Optional.
<T> Optional<T> empty() yields an empty Optional.
<U> Optional<U> flatMap(Function<? super T,? extends Optional<? extends U>> mapper) yields the result of applying mapper to the value in this Optional if present, or an empty optional otherwise.
<U> Optional<U> flatMap(Function<? super T,Optional<U>> mapper) yields the result of applying mapper to the value of this Optional, or an empty Optional if this Optional is empty.