Lecture05 - nus-cs2030/2324-s2 GitHub Wiki

Against null

This lecture serves as a prelude to the topics in the second half of the semester. We focus on the avoidance of null and its associated side-effects by replacing it with Java's Optional. We demonstrate how function objects (which are implementations of different functional interfaces) can be passed to Optional via higher-order methods.

What is null?

Below is an extract of the createUnitCircle method from the maximum disc coverage exercise. Specifically, the method should return a circle only if points p and q are not at the same location and within a distance of 2.0.

Circle createUnitCircle(Point p, Point q) {
    double d = p.distanceTo(q);
    if (d < EPSILON || d > 2.0 + EPSILON) {
        return null;
    }
    ...
    return new Circle(...);
}

What should we return from the method when the condition is not satisfied? In the above, null is returned as there is no better value. But what exactly is null?

null is not an object, nor does it have its own type; but null can be assigned to a variable of any reference (i.e. class or interface) type! One could implement null as a possible value of say, the Null type, and have the Null type be a sub-type of every reference type (i.e. a bottom-type of reference types). But alas, unlike Scala, this is not how Java implements it.

null is a special literal in Java that is designed to be a possible value of any reference typed variable, and here is where the problem lies. Upon calling createUnitCircle, we are not assured that createUnitCircle(..).contains(..) will always work. If createUnitCircle returns a null, we get a NullPointerException and the program breaks!

As an aside, if we do have a Null type, then we would also need to consider the bottom-type of primitives. This could take the form of say, a Nothing type with no value. How is this useful? As an example, a method can return Nothing if it never returns. Suppose we have a method that throws an exception.

... foo() {
    returns new IllegalStateException();
}

In Java and the absence of a Nothing type, you can set the return type to any type! Another example is when your method keeps looping (or recursing),

... foo() {
    return foo();
}

Anyway, we digressed...

Java's Optional Class

Looking at the behaviour of the createUnitCircle method, we expect that the method may return a circle, or return nothing. We can also say that createUnitCircle always returns a box, but inside this box it may either contain a circle or nothing (or an empty box). Java Optional class is exactly the box we need.

Optional is no different from the List or ArrayList. They are just containers of some fixed type. While List<T> and ArrayList<T> are generic containers that hold zero or more elements of type T, Optional<T> is a generic container of zero or one element of type T. In particular, the Optional class has two main factory methods: of(..) and empty(). The former allows you to put something into the box (or wrap something in an Optional) while the latter creates an empty Optional. There is another factory method ofNullable(..) that allows null to be taken in and produces an empty box. On the other hand, of(null) throws an exception.

We can now redefine the createUnitCircle method to have a return type of Optional<Circle>.

Optional<Circle> createUnitCircle(Point p, Point q) {
    double d = p.distanceTo(q);
    if (d < EPSILON || d > 2.0 + EPSILON) {
        return Optional.<Circle>empty();
    }
    ...
    return Optional.<Circle>of(new Circle(...));
}

Low-Level Methods

The Optional class provides low-level methods for low-level programming or first-order programming.

First order programming is where we have data (values or objects) passed and return from methods. As an example, consider the following method to find the coverage given an Optional<Circle> object and a list of points. Specifically, if circle is Optional.empty, it will return a coverage of zero.

int findCoverage(Optional<Circle> circle, ImList<Point> points) {
	int numOfPoints = 0;
	if (circle.isEmpty()) { // or if(!circle.isPresent())
		return 0;
	} else {
		Circle c = circle.get();
		for (Point point : points) {
			if (c.contains(point)) {
				numOfPoints = numOfPoints + 1;
			}
		}
		return numOfPoints;
	}
}

Notice the use of the isEmpty() (or isPresent()) and get() methods of the Optional class. This is a common misuse of the Optional class by beginning students. They see the use of an Optional as another hassle to wrap and unwrap the value contained within, when they could have just coded the if..else statement using null.

	if (circle == null) {
		return 0;
	} else {
		for (Point point : points) {
			if (circle.contains(point)) {
				numOfPoints = numOfPoints + 1;
			}
		}
		return numOfPoints;
	}

This is not the correct way to use Optional as findCoverage method still needs to be mindful of whether the Optional is empty or not. Indeed, in the implementation using Optional, if one were to skip the conditional check and invoke circle.get() straightaway, an exception will still surface if circle is Optional.empty().

Higher-Level Methods

Java's Optional is more than just a data container. In fact, Optional is a kind of safe-box that encapsulates the handling of exceptions within it. In this way, the client that uses Optional correctly does not need to be aware of exceptions!

The idea is to let the client of Optional be able to pass the functionality of finding containment of points in a circle to Optional instead. Higher-level (or higher-order) methods are provided by the Optional to do just that. But first, functions must behave like data, i.e. a function can be assigned to a variable, passed into a method, and also returned from a method. We say that functions are first class citizens. And Java makes use of functional interfaces to allow you to build objects, each containing a function (we sometimes call them function objects).

We shall demonstrate with two examples of higher order methods: filter and map.

filter higher order method

The filter method in Optional class takes in a Predicate which is a generic functional interface with a single abstract method test (sometimes called a SAM interface). The method specified in Predicate<T> is public boolean test(T t). As an example, to test whether a circle contains a fixed point (0.5, 0.5), we need to define an implementation of Predicate<Circle>.

class Containment implements Predicate<Circle> {
	public boolean test(Circle c) {
		return c.contains(new Point(0.5, 0.5));
	}

We can create an instance of Containment and call its test method by passing in some Circle object.

jshell> Predicate<Circle> pred = new Containment()
pred ==> Containment@...

jshell> pred.test(new Circle(new Point(0.0, 0.0), 1.0))
$.. ==> true

The above tests if the circle located at the origin with radius 1.0 contains the point (0.5, 0.5). More importantly, we can now pass an instance of Containment into the filter method of Optional:

createUnitCircle(..).filter(pred)

Depending on the outcome of createUnitCircle, if it returns an Optional containing a valid circle, then filter will check whether the point (0.5, 0.5) is contained within. If it does, then the same Optional is returned from filter; otherwise Optional.empty() is returned. On the other hand, if Optional.empty() is returned from createUnitCircle, then filter just returns it. Here are some more examples.

jshell> createUnitCircle(new Point(-1.0, 0.0), new Point(1.0, 0.0)).
   ...> filter(pred)
$.. ==> Optional[circle of radius 1.0 centred at point (0.000, 0.000)]

jshell> createUnitCircle(new Point(-1.0, 10.0), new Point(1.0, 10.0)).
   ...> filter(pred)
$.. ==> Optional.empty

jshell> createUnitCircle(new Point(1.0, 0.0), new Point(1.0, 0.0)).
   ...> filter(pred)
$.. ==> Optional.empty

Notice that onus on checking the presence or absence of a circle no longer lies with the client, but on Optional instead. In other words, Optional handles missing values for the client.

You may also realize that this is not the first time we are defining an implementation of a functional interface and passing to a higher order method. Recall the Comparator interface and how it was passed to the sort method of a list.

We can certainly pass different test functionalities to the filter method. But do we need to create different classes every time?

One way of writing the implementation of a functional interface is to use an anonymous inner class (or class with no name).

jshell> Predicate<Circle> pred = new Predicate<Circle>() {
   ...>    public boolean test(Circle c) {
   ...>       return c.contains(new Point(0.5, 0.5));
   ...>    }
   ...> }
pred ==> ..@..

jshell> createUnitCircle(new Point(-1.0, 0.0), new Point(1.0, 0.0)).
   ...> filter(pred)
$.. ==> Optional[circle of radius 1.0 centred at point (0.000, 0.000)]

Yet another simpler way is to write a lambda expression.

jshell> Predicate<Circle> pred = c -> c.contains(new Point(0.5, 0.5))
pred ==> $Lambda$...

jshell> createUnitCircle(new Point(-1.0, 0.0), new Point(1.0, 0.0)).
   ...> filter(pred)
$.. ==> Optional[circle of radius 1.0 centred at point (0.000, 0.000)]

Here we do not even need to provide the header of the test method; just the body will suffice. This is possible because a functional interface has only one abstract method to be defined.

Notice that the test method in Predicate needs to read data from the Circle. That is to say data flows into Predicate. From the discussion of data flow in the previous lecture, we see that the filter method should be declared with the contravariant parameter Predicate<? super T> instead so as to support more types of predicates.

filter :: Optional<T> -> Predicate<? super T> -> Optional<T>

Using the Circle example, the predicate passed to filter need not only read the Circle as a Circle, but can also read it as an Object!

jshell> Predicate<Object> pred = x -> x.equals(new Circle(new Point(0.0, 0.0), 1.0))
pred ==> $Lambda$...

jshell> createUnitCircle(new Point(-1.0, 0.0), new Point(1.0, 0.0)).
   ...> filter(pred)
$.. ==> Optional[circle of radius 1.0 centred at point (0.000, 0.000)]

jshell> pred = x -> x.equals(new Point(0.0, 0.0))
pred ==> $Lambda$...

jshell> createUnitCircle(new Point(-1.0, 0.0), new Point(1.0, 0.0)).
   ...> filter(pred)
$.. ==> Optional.empty

The above first tests if the Optional<Circle> returned from createUnitCircle is equals to a circle located at the origin with radius 1.0, and then tests if it is equals to a point located at the origin.

map higher order method

The map method in Optional takes in an instance of some implementation of the Function functional interface with the specification of the single abstract method apply. The apply method is associated with an input type T and an output type R.

As an example consider mapping a circle to its radius (assuming the presence of a getRadius() method). We demonstrate the creation of the function by defining an anonymous inner class, as well as a lambda expression.

jshell> Function<Circle, Double> f = new Function<Circle, Double>() {
   ...>    public Double apply(Circle c) {
   ...>       return c.getRadius();
   ...>    }
   ...> }
f ==> ..@..

jshell> f.apply(new Circle(new Point(0.0, 0.0), 1.0))
$.. ==> 1.0

jshell> f = c -> c.getRadius()
f ==> $Lambda$..

jshell> f.apply(new Circle(new Point(0.0, 0.0), 1.0))
$.. ==> 1.0

Likewise we can pass a Function<Circle,Double> function object into Optional<Circle>.

jshell> createUnitCircle(new Point(-1.0, 0.0), new Point(1.0, 0.0)).
   ...> map(c -> c.getRadius())
$.. ==> Optional[1.0]

jshell> createUnitCircle(new Point(-1.0, 0.0), new Point(1.0, 0.0)).
   ...> map(c -> c.contains(new Point(0.5, 0.5)))
$.. ==> Optional[true]

jshell> createUnitCircle(new Point(1.0, 0.0), new Point(1.0, 0.0)).
   ...> map(c -> c.contains(new Point(0.5, 0.5)))
$.. ==> Optional.empty

Notice that the map method of Optional applies the Function to the object inside the box, while retaining the box. Observe that calling map results in another Optional, containing the mapped value, possibly of a different type.

What happens if we have a Function defined as follows:

jshell> Function<Object,Integer> f = x -> x.toString().length()
f ==> $Lambda$..

jshell> Optional<Number> on = createUnitCircle(new Point(-1.0, 0.0), new Point(1.0, 0.0)).
   ...> map(f)
on ==> Optional[62]

jshell> Optional<Number> on = createUnitCircle(new Point(1.0, 0.0), new Point(1.0, 0.0)).
   ...> map(f)
on ==> Optional.empty

Notice that createUnitCircle returns Optional<Circle> from which the map method was invoked that takes in Function<Object,Integer>, with the return value assigned to a variable of type Optional<Number>. There are four different type parameterizations here!

The reason why this works is because the map method is declared with a Function parameter where function inputs T are contravariant and function outputs R are covariant.

map :: Optional<T> -> Function<? super T, ? extends R> -> Optional<R>

In our example, T is bound to Circle which is the outcome of createUnitCircle. R is bound to Number based on the assignment to the on variable. The Function passed to map is expected to read in data from the circle, and give out data in the form of a Number. Since data is flowing into Function with type T, it can also be read as ? super T. Likewise, if Function is expected to have data of type R flowing out, it can also give out data as ? extends R. Take some time to digest this.

Declarative programming with higher order methods

Now we have an alternative way to define the findCoverage method.

int findCoverage(Optional<Circle> circle, ImList<Point> points) {
	int numOfPoints = 0;
	for (Point point : points) {
		numOfPoints = numOfPoints + circle
			.filter(c -> c.contains(point)) // Optional<Circle>
			.map(x -> 1) // Optional<Integer>
			.orElse(0); // Integer
	}
	return numOfPoints;
}

In the above, finding coverage of a particular point within the circle requires passing the functionality of containment testing via the filter method, and then passing the functionality of mapping each circle to the value 1 via the map method. The orElse method if necessary as it will return the value 1 within the non-empty Optional, or a default value of 0 if map returns Optional.empty. In this way, summing up the 1s and 0s will give the coverage over all points.

It is important to note that the existence of an object in an Optional container is encapsulated within the Optional and never exposed to the client, at least not until the final integer value is needed for the purpose of summing as part of iterating over the points.

As one can envision, map, filter and other higher order methods are useful design patterns that one can employ to solve computation problems. Many problems will require us to deal with missing values and we can now rely on the creation of Optional objects to help us manage them. The only difference would be the functionalities that we pass to the higher order methods. This is in line with the abstraction principle [Benjamin C. Pierce]:

Each significant piece of functionality in a program should be implemented in just one place in the source code. Where similar functions are carried out by distinct pieces of code, it is generally beneficial to combine them into one by abstracting out the varying parts.

With respect to say the map method in Optional, mapping only when a value is present is the "similar function" that is common throughout all problems, and hence implemented in the map method. On the other hand, the "varying parts" are the specific mapping functions that are abstracted out, which will be different depending on the specifics of the problem.

As we mentioned, this is just a prelude to the topics that we are going to cover in the later half of the semester. And just to whet your appetite, you will see in due time that we can further refactor the implementation of findCoverage by encapsulating the iteration of points. In other words, we no longer need to write loops!

int findCoverage(Optional<Circle> circle, ImList<Point> points) {
    return circle
        .map(x -> points.stream()
            .filter(y -> x.contains(y))
            .map(y -> 1)
            .reduce(0, (a,b) -> a + b))
        .orElse(0);
}

Comparing this new implementation with the original one below

int findCoverage(Optional<Circle> circle, ImList<Point> points) {
	int numOfPoints = 0;
	if (circle.isEmpty()) { // or if(!circle.isPresent())
		return 0;
	} else {
		Circle c = circle.get();
		for (Point point : points) {
			if (c.contains(point)) {
				numOfPoints = numOfPoints + 1;
			}
		}
		return numOfPoints;
	}
}

you will see two distinctly different ways of programming. The original implementation focuses on state changes while the program runs. In particular, the state of the loop variable point, as well as the state of numOfPoints change at every loop. This is the usual imperative programming paradigm that students have already been acquainted with since programming 101, where one needs to express how to do something and be always mindful of the changes in state of the program as it runs.

In contrast, our new implementation is coded using a declarative programming paradigm. One need only tell Optional and also Stream what to do via the higher order methods. There are no state changes; indeed there is no assignment at all!

⚠️ **GitHub.com Fallback** ⚠️