Functional Programming

Functional Programming is a programming paradigm (Object-oriented programming is another paradigm) where we treat functions like first-class citizens. What we mean by this is that functions can be treated as variables, passes as arguments, and even returned by functions. In this unit, we will discuss how Java enables “functional-like programming”, how it differs from true functional programming, and how to use functional programming principles in Java.

Python Functional programming

We can actually show functional programming in Python:

def my_func(input):
    print("I am a function")
    print("My input is", input)
    
x = my_func
print(x)
x(10)

This prints:

<function my_func at 0x00000178068A2E18>
I am a function
My input is 10

That is, we assigned the variable x to the function my_func. The first line prints where in memory the function instructions are stored. The second and third printing statements are inside of the function, which we call by calling x(10).

Unfortunately, Java does not support treating functions as first-class citizens. That is, we cannot assign functions to variables, pass them as arguments, etc.

What Java has: SAMs

However, Java does support a workaround that lets us get closer to functional programming via Functional Interfaces. Functional interfaces are interfaces that can be used to define a single abstract method SAM. One example of a SAM interface you have seen is Comparator.

public class StringIgnoreCaseComparator implements Comparator<String> {
    public int compare(String s1, String s2) {
        s1 = s1.toUpperCase();
        s2 = s2.toUpperCase();
        return s1.compare(s2);
    }
}

A Comparator is an interface that defines one function, compare, which takes in two instances of a particular class and returns a number indicating which item should go first if sorted. For example, if I call compare(a, b):

  • Positive - a comes after b when sorted
  • Negative - b comes after a when sorted
  • Zero - a and b are equal

Thus, can think of each class that implements Comparator as a separate way to sort. In this particular case, I am sorting Strings in a way that ignores case (since, by default, String sorting is case-sensitive, with all capital letters coming before all lowercase letters).

This class is, effectively, a wrapper for a single function. Because this is a class, we can make instances of this class and pass it to functions expecting a Comparator. For example:

    List<String> words = getWordsAsListFromFile("myfile.txt");
    words.sort(new StringIgnoreCaseComparator());

The function sort is a method on List that takes in a Comparator and sorts the list using that Comparator’s compare function.

In this way, we can think of new StringIgnoreCaseComparator() as effectively a function, even if syntactically it is a class.

Class Explosion

One downside of this approach, however, is the potential for an explosion in the number of classes. For example, take the following class:

public class UVAStudent implements Comparable<UVAStudent> {
    private final int id;
    private String firstName, lastName;
    private final String computingID;
    private StudentType studentType;
    private double gpa;

    public UVAStudent(int id, String firstName, String lastName, String computingID, double gpa, StudentType studentType) {
        this.id = id;
        this.firstName = firstName;
        this.lastName = lastName;
        this.computingID = computingID;
        this.gpa = gpa;
        this.studentType = studentType;
    }
    //getters for all fields, setters for all non-final fields
    ...
}

This data structure class represents a student at UVA. Imagine we were making a GUI with a table of all students that lets us sort by any field above. Well, we have 6 fields, so this means at least 6 Comparator classes (12 if you also want reverse). All of these classes only exist for the sake of sorting a List of UVAStudent objects, yet will pollute our namespace. Now imagine our app has even more data classes, where each one also has Comparators for each field, and you can see how this blows ups quickly.

Anonymous classes

One way around this is the anonymous class. That is, rather than write StudentIdComparator as a separate class in the package, we can define the class in-line in our code.

public class UVAStudentManager {
    
    public void getStudentsSortedById(List<UVAStudent> studentList) {
        studentList.sort(new Comparator<UVAStudent>() {
            @Override
            public int compare(UVAStudent o1, UVAStudent o2) {
                return Integer.compare(o1.getId(), o2.getId());
            }
        });
    }
}

Here, wee have created an anonymous class. We define a class in-line inside of the sort Function with:

    new Comparator<UVAStudent>() {
        @Override
        public int compare(UVAStudent o1, UVAStudent o2) {
            return Integer.compare(o1.getId(), o2.getId());
            }
    }

This lets us create an instance of a new class when we need it. The class doesn’t have a name in the global namespace. This feature works well with functional interfaces, when we don’t want a long-term class that has several instances, but instead are simply using the class as a wrapper for a function we want.

However, that syntax is still a bit long. Instead, wouldn’t it be great if we could write something like this?

    public void getStudentsSortedById(List<UVAStudent> studentList) {
        studentList.sort((UVAStudent s1, UVAStudent s2) -> Integer.compare(s1.getId(), s2.getId()));
    }

That is, we just define a function in-line: a function takes in two students, and compares them by their id.

Well it turns out we can. Because the code above, as written, works. It sorts student in ascending order by their ID.

Lambda functions

A lambda function is an unnamed function. Rather than naming a function, we create a function when it’s needed on the fly at runtime.

The general structure of a lambda statement is either:

(parameters) -> {code block}

For example, we can take our earlier StringIgnoreCaseComparator and define the compare function as a lambda body:

    Comparator<String> ignoreCase = (String s1, String s2) -> {
        s1 = s1.toUpperCase();
        s2 = s2.toUpperCase();
        return s1.compareTo(s2);
    };

From there, we can shorten this a bit. For example, we know that the types of s1 and s2 can only be String because we are making a Comparator<String>. And so, we don’t need to tell Java the datatype: Java will figure it out:

    Comparator<String> ignoreCase = (s1, s2) -> {
        s1Upper = s1.toUpperCase();
        s2Upper = s2.toUpperCase();
        return s1Upper.compareTo(Upper);
    };

If we wanted, we could shorten the code block to one-line.

Comparator<String> ignoreCase = (s1, s2) -> {
    return s1.toUpperCase().compareTo(s2.toUpperCase());
};

From there, we can actually go one step farther. Another valid format is:

    (parameters) -> expression

In this case, whatever expression evaluates to is returned, without needing to invoke the return keyword. For example:

(int x, int y) -> x + y

Is a lambda function for getting the sum of two numbers. In this case, whatever x and y add up to will be returned

Using that, going back to our ignoreCase Comparator, we can say:

Comparator<String> ignoreCase = (s1, s2) -> 
        s1.toUpperCase().compareTo(s2.toUpperCase());

To test this out, we can make a List of strings and sort it…

    List<String> words = new ArrayList<>(List.of("Apple", "Zebra", "banana", "Catfish", "aardvark"));
    words.sort((s1, s2) -> s1.toUpperCase().compareTo(s2.toUpperCase()));
    System.out.println(words);

…and it will print:

[aardvark, Apple, banana, Catfish, Zebra]

Quick note:

If a lambda body has exactly one argument, you do not need parentheses around the first value (they are optional).

(x) -> x * x x -> x * x

Both of the above work, and both define a lambda for squaring an input number.

We’ve seen this before

Think back to assertThrows in our testing unit:

@Test
public void testWithdrawInsufficientFunds() {
    BankAccount account = new Account(500);
    
    assertThrows(InsufficientFundsException.class, ()-> 
        account.withdraw(700));
}

If you look at the official javadoc for the assertThrows method, you’ll see the signature is:

public static <T extends Throwable> T assertThrows(Class<T> expectedType, Executable executable)

The JUnit 5 interface Executable, like Comparator is a functional interface. Like all other functional interfaces, it has one method:

public void execute()

This method takes in nothing, and returns nothing. So, when we write…

()-> account.withdraw(700)

…we are defining a function that takes in nothing, and simply calls account.withdraw(700). And we are asserting that, while executing this lambda function, an InsufficientFundsException will be thrown.

We could have written this out as:

    @Test
    public void withdraw_InsufficientFunds_Exception(){
        //define executable that creates exception 
        Executable testExecutable=new Executable() {
            @Override
            public void execute() throws Throwable{
                testAccount.withdraw(600);
            }
        };
        
        assertThrows(InsufficientFundsException.class,testExecutable);
        assertEquals(500,testAccount.getBalance(),1e-4);
    }

However, the above code is arguably harder to read than:

    assertThrows(InsufficientFundsException.class, () -> testAccount.withdraw(600));
    assertEquals(500, testAccount.getBalance(),1e-4);

This is because there is more text that is extraneous, or unnecessary. For example Executable, new Executable() , @Override, etc. All of this introduces accidental complexity to our test that can be removed. Since both do the same thing, we use the second version.

In essence, we want our code to only include that which is needed. We want to highlight the function and hide the class, since the class only exists to be a vessel for the Function.

Functional interfaces to know:

There are a few functional interfaces it’s a good idea to be generally familiar with. These are interfaces that have one function (some have static “default” functions, but that isn’t usually relevant to using the class).

Nearly all of these interfaces have some equivalent interface in other languages that don’t support true functional programming, like Kotlin, C#, etc. Other languages that do allow functions to be passed/assigned as variables (Python, Typescript, C++, for example) will still use interfaces to describe expect function inputs/outputs.

Comparator

method: public int compare(E e1, E e2)

use: used in sorting lists, specifically Collections.sort(List<E> list, Comparator<E> comparator) and listInstance.sort(Comparator<E> comparator). Also used in defining TreeSets.

example: (a, b) -> a - b

Assuming a and b are number types (int, double, etc.), would be used to sort number in ascending order.

Executable (JUnit5)

method: public void execute()

use: used in assertThrows, dynamicTest and assumingThat.

example: () -> student.enroll(cs3140)

Defines an executable where a student enrolls in a given class. Specifically in JUnit, this is primarily used in assertThrows to check for an Exception being thrown.

Be aware that there is a separate Java class called Executable which is-not a functional interface. Java, however, does have an equivalent functional interface called Runnable which you can use with Threads.

Runnable (JUnit5)

method: public void run()

use: used to describe a procedure to be executed, typically by a Thread.

example: Thread newThread = new Thread(() -> myRunFunction())

Threading is a topic that, at the University of Virginia, is taught in CS 2100, and so, as yet, I do not have a section on it in this textbook. It is something I’m considering adding eventually.

Predicate

method: public boolean test(E e)

use: used for checking if a value meets some condition. Specifically, it is used in the filter method for Streams (covered in the next module).

example: (student) -> student.getGPA > 3.5

The above returns true for students with at least a 3.5 would be eligible for the Dean’s List at UVA.

Consumer

method: public void accept(E e)

use: Take in some value and doing something with it, but don’t return anything. Used specifically in Java’s foreach function on Collections, as well as foreach and peek in Streams (next module).

example: (value) -> System.out.println(value)

This can allow us to replace:

    for(Item item : itemList) {
        System.out.println(item);
    }

With:

itemList.forEach(item -> System.out.println(item))

Supplier

method: public E get()

use: get results from some source and do something with it. This function does not take in any input.

example: () -> (int) (Math.random() * 6) + 1

This function generate a random integer from 1-6 (rolling a six-sided die);

Function<E, R>

method: public R apply(T)

use: pass some instance of T as an argument, return an instance of R. Used in the map function in Java Streams.

example: x -> x.toString().toUpperCase()

A function that takes in some object x and returns its String representation as an uppercase String.

A Function<Double, Integer> would take as input a Double and return an Integer. We could, for instance, implement a rounding function: d -> (int)(d + 0.5)

ActionListener

method: public void actionPerformed(ActionEvent e)

use: Used in event-driven applications to respond to user interactions. We will see this in our GUI unit working with JavaFX.

example: e -> handleButtonPress()

Structurally, the same thing as Consumer<ActionEvent>, its more specialized for event-driven applications (typically GUIs).

Method Captures

Taking a second, let’s look back at our Consumer example:

itemList.forEach(item -> System.out.println(item))

In this case, you’ll notice that the input to our Consumer is exactly the same as our input to System.out.println. That is, our consumer is simply passing our existing input to another function. Whenever we are using a lambda body to simply pass our input to another function that expects the same input, we can use a method capture.


That is, we simply use `System.out.println()` **as the Consumer function**.

Another example of this can be seen with `Comparator`.

Say we had the following code for a given `List<UVAStudent>`:

```java
    studentList.sort((x, y) -> {
            if (x.getLastName().equals(y.getLastName())) {
                return x.getFirstName().compareTo(y.getFirstName());
            } else {
                return x.getLastName().compareTo(y.getLastName());
            }
        });

This lambda-body is too big. But this doesn’t mean we can’t use this function in a lambda body. First, we can definite a function in our StudentManager class:

    public int compareLastNameThenFirstName(UVAStudent s1, UVAStudent s2) {
        if (s1.getLastName().equals(s2.getLastName())) {
            return s1.getFirstName().compareTo(s2.getFirstName());
        } else {
            return s1.getLastName().compareTo(s2.getLastName());
        }
    }

And now we can invoke that function in our lambda body:

public class UVAStudentManager {
    public void sortStudentsLastNameThenFirstName(List<UVAStudent> studentList) {
        studentList.sort(this::compareLastNameThenFirstName);
    }

    public int compareLastNameThenFirstName(UVAStudent s1, UVAStudent s2) {
        if (s1.getLastName().equals(s2.getLastName())) {
            return s1.getFirstName().compareTo(s2.getFirstName());
        } else {
            return s1.getLastName().compareTo(s2.getLastName());
        }
    }
}

Note that we use this because we are calling that function on our instance. However, because the function compareLastNameThenFirstName doesn’t use any this instance variables (it only uses the functions inputs), we can safely make the function static. Because the function is static, we can refer to it by its class name:

public class UVAStudentManager {
    public void sortStudentsLastNameThenFirstName(List<UVAStudent> studentList) {
        studentList.sort(UVAStudentManager::compareLastNameThenFirstName);
    }

    public static int compareLastNameThenFirstName(UVAStudent s1, UVAStudent s2) {
        if (s1.getLastName().equals(s2.getLastName())) {
            return s1.getFirstName().compareTo(s2.getFirstName());
        } else {
            return s1.getLastName().compareTo(s2.getLastName());
        }
    }
}

We will look more at lambda bodies and method captures in the next unit.


Previous submodule:
Next submodule: