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.
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
staticList 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
staticList 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
.
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.
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
and etc.
The functional interface
approach also does not support partial application of functions.
Not surprisingly, this won't compile
BiFunctionf2 = (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
The function interface
approach does not play well with recursion. We need some
form of indirection to implement the following idea:
void foo() { Functionf = i -> i <= 0 ? true : f.apply(i - 1); // ^ there's no reference to `f` f.apply(5); }
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