4.12.3.1 - Functional lists, heads, tails and construction (Java)

4.12.3.1 - Functional lists, heads, tails and construction (Java)

Represent a functional list as a head followed by a tail, apply each required list operation, and construct results without changing the original list. Use these operations in a small recursive Java program.

Java route status: incomplete: runnable support pending. Native written code tasks have no Run. Functional notation and Java implementation remain distinct throughout this Paper 2 lesson.

Head, tail and empty lists

A non-empty list can be separated into its first element and the list that remains.

Head and tail

The head is the first element of a non-empty list. The tail is the list of all remaining elements, in their original order. The head is an element; the tail is a list, including when that list is empty.

For [4,3,5][4,3,5], the head is 44 and the tail is [3,5][3,5]. Using the Haskell-style head:tail representation,

[4,3,5]=4:[3,5]=4:(3:(5:[])).[4,3,5]=4:[3,5]=4:(3:(5:[])).

Here the colon constructs a list by placing an element in front of a list. It is functional list notation, not Java syntax. The final [][] is an empty list, which finishes the structure. This explains why a list can be built from repeated head-plus-tail construction.

For [9][9], the head is 99 and the tail is [][]. The empty list itself has no first element: head and tail operations on [][] are undefined in this model. Do not invent a head of 0 or treat an empty tail as the number 0. The program must check emptiness before requesting a head or tail when emptiness is possible.

Java's List<Integer> contains integer objects. xs.get(0) obtains its head when the list is non-empty. xs.subList(1, xs.size()) describes the tail portion, but it is a view backed by the original list. Our functional implementation copies it so the returned list does not share that mutable view:

Java
static Integer head(List<Integer> xs) {
    if (xs.isEmpty()) {
        throw new IllegalArgumentException("Empty list has no head");
    }
    return xs.get(0);
}

static List<Integer> tail(List<Integer> xs) {
    if (xs.isEmpty()) {
        throw new IllegalArgumentException("Empty list has no tail");
    }
    return Collections.unmodifiableList(
        new ArrayList<>(xs.subList(1, xs.size())));
}

These methods belong inside Main, with List, Collections and ArrayList imported. unmodifiableList prevents changing the returned list through that reference. Because the elements here are integer values, we need not consider changes inside mutable element objects. The methods do not modify xs.

Length and empty checks

Testing for emptiness and obtaining a length are valid for every list, including an empty list.

An empty check returns a Boolean. A length operation returns the number of elements, not the final index and not the sum of their values. Thus empty⁡([])=true\operatorname{empty}([])=\mathrm{true}, length⁡([])=0\operatorname{length}([])=0, and length⁡([4,3,5])=3\operatorname{length}([4,3,5])=3. Repeated elements each count: [7,7][7,7] has length 2.

Java expresses these operations as xs.isEmpty() and xs.size(). Collections.emptyList() constructs an empty list value of the inferred element type; it does not construct a list whose only element is null or zero.

Java
List<Integer> empty = Collections.emptyList();
List<Integer> one = Collections.singletonList(0);
System.out.println(empty.isEmpty());
System.out.println(empty.size());
System.out.println(one.isEmpty());
System.out.println(one.size());

These statements belong inside main with the required imports. The outputs are true, 0, false, 1. A list containing 0 is not empty. Neither is a list containing an empty list: its outer list has one element, though that element's own length is 0.

Emptiness gives a base case for recursive processing. In a head–tail definition of length, length⁡([])=0\operatorname{length}([])=0, while a non-empty list has length 1+length⁡(tail⁡(xs))1+\operatorname{length}(\operatorname{tail}(xs)). Each call processes a shorter tail, so it eventually reaches the empty list. The library's size() supplies the same required operation without making a recursive call in our code.

Input categoryEmpty checkLengthSafe head/tail?
[][]true0No
[0][0]false1Yes; tail is [][]
[3,3,8][3,3,8]false3Yes; tail is [3,8][3,8]

These boundary cases distinguish list structure from the values stored in it.

Prepending and appending

Prepending and appending construct a list with one new element at different ends.

Prepend places an item before every original element: prepend⁡  7  [4,3,5]=[7,4,3,5]\operatorname{prepend}\;7\;[4,3,5]=[7,4,3,5]. This is the same construction as 7:[4,3,5]7:[4,3,5]. Append places an item after every original element: append⁡  7  [4,3,5]=[4,3,5,7]\operatorname{append}\;7\;[4,3,5]=[4,3,5,7]. Appending the item 7 differs from appending a whole list as one nested item.

Both operations on an empty list produce the singleton list [7][7]. On a non-empty list they usually produce different orders. Both increase length by one; neither removes repetitions. Functional construction leaves the original list available unchanged.

Continue this lesson.

You’re previewing NovaLearn without an account. Create a free account to read the rest.

Create free account