Lambdas

From CSE231 Wiki
Jump to navigation Jump to search

Lambdas are one of the most recently-added features in Java, essentially allowing one to pass a function as an parameter to a method. The functionality provided by lambdas has always been available, but lambdas allow the syntax to be significantly more succinct. This page will discuss alternatives to lambdas in order to better explain what lambdas are doing behind the scenes. It is based on the Java Tutorials, but has been adapted for the code used more frequently in CSE 231. If you are unfamiliar with interfaces, you should probably read about them before this page.

The Problem

In Habanero-Java, the async() method is used to spawn a task that will run asynchronously to the rest of your program. Ideally, the async() method would take a function as a parameter. The async() method could then call this function in parallel with the rest of your code. In Java, however, functions cannot be passed as parameters to methods. Only objects (and primitive types) can be passed in.

To solve this problem, the async() method takes an HjSuspendable as a parameter. HjSuspendable is an interface which contains a single method: run(). You can read more about interfaces on the Interfaces page. The async() method requires you to pass in an instance of a class that implements HjSuspendable--thus guaranteeing that it has a run() method--and the async() method will call the run() method of your object asynchronously.

Named Classes

A straightforward solution is to write your own class that implements HjSuspendable. Let's say, for example, that you want to print "Hello, World!" asynchronously. The pseudocode for this would be something like this:

async {
  print "Hello, World!"
}

We could create a class called PrintHelloWorld that implements HjSuspendable. It would look something like this:

public class PrintHelloWorld implements HjSuspendable {

    @Override
    public void run() throws SuspendableException {
        System.out.println("Hello, World!");
    }

}

An instance of this class could then be passed into the async() method. For example:

HabaneroClassic.async(new PrintHelloWorld());

This code would perform the way we would expect from the pseudocode.

Now let's try a more complex example. Let's try to recreate the ArraySum program using this method. The pseudocode for this program would look like this:

finish {

    async {
        leftSum = 0
        for j in [0, midpoint) {
            leftSum += array[j]
        }
    }

    rightSum = 0;
    for j in [midpoint, array.length) {
        rightSum += array[j]
    }

}

print leftTotal + rightTotal

For this program, we'd need to write two classes: one for the HjSuspendable object passed to the finish() method, and one for the HjSuspendable object passed to the async() method. There is an issue, however. Several variables need to be accessed both inside and outside of the async. The array, instantiated outside of the finish, must be accessible within both the finish and the async. The leftTotal and rightTotal variables, instantiated inside of the finish, need to be accessible outside of it. Because we're creating a new class, this can easily be accomplished by creating instance variables within our new classes. The code would look something like this:

class SumLeftHalf implements HjSuspendable {

    int[] array;
    int midpoint;

    int leftSum;

    public SumLeftHalf(int[] array) {
        this.array = array;
        midpoint = array.length / 2;
    }

    public void run() {
        for (int i = 0; i < midpoint; i++)
            leftSum += array[i];
    }

    int getLeftSum() {
        return leftSum;
    }

}
class AsyncArraySum implements HjSuspendable {

    int[] array;
    int midpoint;

    SumLeftHalf sumLeftHalf;
    int rightSum;

    AsyncArraySum(int[] array) {
        this.array = array;
        midpoint = array.length / 2;
    }

    public void run() {

        sumLeftHalf = new SumLeftHalf(array);
        async(sumLeftHalf);

        for (int j = midpoint; j < array.length; j++)
            rightSum += array[j];

    }

    int getSum() {
        int leftSum = sumLeftHalf.getLeftSum();
        return leftSum + rightSum;
    }

}
AsyncArraySum arraySum = new AsyncArraySum(array);
finish(arraySum);
System.out.println(arraySum.getSum());

Note the incredible bulkiness of this code. This code is significantly longer than the pseudocode, and part of this stems from the fact that we are writing entire classes to contain methods. The other issue is that a method in one class does not have access to the local variables in another class. This fact requires us to create methods and fields to store and access these variables, making the code a lot less readable.

Nested Classes

One way to organize this code a bit better would be to make the AsyncArraySum and SumLeftHalf classes nested inside the original class. This would make the code a bit more readable and would also provide more access to local variables. You can read more about nested classes from the Java Tutorials.

Anonymous Classes

Anonymous classes have existed in Java since Java 1.1, and they've been the best solution to this problem up until Java 8. Anonymous classes allow you to simplify your code by declaring an unnamed class within the method of another class. Anonymous classes are good for short, one-time-use-only classes, exactly what we're looking for here. You can read more about anonymous classes from the Java Tutorials. Here is some example syntax for our code:

int mid = array.length / 2;
int[] subSums = new int[2];

finish(new HjSuspendable() {

    public void run() {

        async(new HjSuspendable() {

            public void run() {
                for (int i = 0; i < mid; i++) {
                    subSums[0] += array[i];
                }
            }

        });

        for (int j = mid; j < array.length; j++) {
            subSums[1] += array[j];
        }
    }

});
System.out.println(subSums[0] + subSums[1]);

Notice that the array and subSums variables can be accessed within and outside of the anonymous class. This is a key feature that anonymous inner classes have that regular classes don't. Java allows us to access variables in an entirely different scope, so long as they are final or effectively final. What it is doing behind the scenes is functionally similar to our named classes example. It passes a reference to the subSums array, and it doesn't allow you to change the reference in the enclosing class because it would be too complicated to then change it in the anonymous class instance. This is a key limitation to anonymous classes, and it leads to some annoying issues in HW1.

Lambdas

Lambdas, added in Java 8, make the syntax for anonymous classes significantly simpler. They are allowed only when an interface has a single method. In this case, the HjSuspendable class has only one method: run(). The syntax using lambdas would look like this:

int mid = array.length / 2;
int[] subSums = new int[2];

finish(() -> {

    async(() -> {
        for (int i = 0; i < mid; i++) {
            subSums[0] += array[i];
        }

    });

    for (int i = mid; i < array.length; i++) {
        subSums[1] += array[i];
    }

});
System.out.println(subSums[0] + subSums[1]);

Lambdas act very similar to anonymous classes, but the syntax is significantly clearer. Our code is easier to read and it makes sense what it's doing on a high level--passing a method as a parameter to another method.