daniberg.com

posts github chip8 rtc guitar

Java lambdas

Lambdas in java are based on the idea of functional interfaces which we define as an interface with a single abstract method.

Here we describe a few of the shortcomings of the functional interface approach taken by java to implement lambdas.

Syntax

We take the Predicate function defined in the standard library as an example. Predicate as a functional interface provides the abstract method test
@FunctionalInterface
public interface Predicate {
    boolean test(T t);
}

Filters are a common use case of predicate functions and can be implemented as follows

static  List filter(List xs, Predicate p) {
    // ... impl
    return xs;
}

Syntax wise the reader of the filter function signature is unaware that p is a lambda construct. Even if the reader has a hint that Predicate might be a function there's no information about the "shape" of the function.

We can update the syntax to clearly identify lambdas and their "shape". For example

static  List filter(List xs, T -> Boolean p) {
    // ... impl
}

It's now clear that p is a function and that it's a function that receives a type T and returns a type Boolean.

Function application

The functional interface approach also hinders code re-use and function composition. Currently, the application of a function requires the name of the abstract method defined in the interface.

For instance, function composition can be declared as follows

static  Function compose(Function f1, Function f2) {
    return (A a) -> f2.apply(f1.apply(a));
}

The current approach binds the interface Function and its method apply which decreases code re-use. If we have a function with the same shape in our code base but with a name other than apply we cannot use compose directly.

On the other hand, this restriction is lifted if function application in lambdas is modified to follow the rules of regular methods.

Piggybacking on our made up lambda syntax from the previous example and removing the interface method during function application we can define composition as follows

static  A -> C compose(A -> B f1, B -> C f2) {
    return (A a) -> f2(f1(a))
}

After the changes, any function in our code base that satisfies the arity and types of compose can be composed.

Package java.util.function

The package java.util.function illustrates the problem of diminished opportunity for code re-use and the increase of cognitive load.

The documentation for java 8 describes 44 function interfaces. Is the package lacking in different types of functions? Or, 44 functions is all we need?

Take Function as a base case. What if we need a function that accepts 2 parameters? BiFunction is provided. What if we need 3 parameters? There's no TriFunction in the standard library and we're on our own.

The situation is made worse if we count the implementation specific to primitive types: IntFunction, LongFunction and etc.

Partial application

The functional interface approach also does not support partial application of functions.

Not surprisingly, this won't compile

BiFunction f2 = (i, j) -> i + j;
Function f = f2.apply(1);

In our hypothetical syntax we can make the case for

Integer -> Integer -> Integer f2 = (i, j) -> i + j;
Integer -> Integer f1 = f2(1);
int r = f1(2); // r is set to 3

Recursion

The function interface approach does not play well with recursion. We need some form of indirection to implement the following idea:

void foo() {
    Function f = i ->
        i <= 0 ? true : f.apply(i - 1);
    //                  ^ there's no reference to `f`
    f.apply(5);
}

Exception propagation

Typed exceptions are also not propagated.

The following code won't compile

void bar() throws Exception {
    Optional.of(10).map(this::baz);
}

int baz(int i) throws Exception {
    if (i < 0) throw new Exception("");
    return i;
}

Our initial assumption is that baz throws an Exception, and thus it would be correct to expect that bar also throws an exception since baz is used in the map operation by bar.

©2023 daniberg.com