Lambda Functions

COMP 310    Java Resources  Eclipse Resources
Reference: Oracle documentation on Lambda Expressions

"Lambda" or "anonymous" functions are simply the notion of functions without names.  That is, a lambda function is an entity that represents some sort of computational behavior, independent of any name associated with it.   In particular, a lambda function embodies the concept of functionality being something that can be passed around the system as a value, i.e. functionality being a "first class entity".

Lambda functions are at the very heart of computer science, being first described by one of the founders of modern computer science theory, Alonzo Church, in his formulation of "lambda calculus".    In a nutshell,. Church showed that anything in program can be constructed from lambda functions, including some unexpected things, such as numbers and variables.

Java, starting with Java 8, introduced an implementation of lambda functions, or as Oracle calls them "lambda expressions" (which is technically closer to their implementation).   One can think of Java's lambda implementation as a shortcut syntax for anonymous inner classes that implement interfaces with a single method.  However, lambda functions bring a new concept to Java: contextually-based inferred typing.    While generics in Java use inferred typing to determine the types of generically typed variables, the types in a lambda function are dtermined by looking at the usage of the lambda.   That is, the same lambda expression could be used in two different situations and where tthe result would be two different interfaces being implemented where the interface name, the method name and the method types could all be different, though the number of input parameters would have to be the same.

Syntax

Lambda functions in Java assume that there is a "functional" interface defined somewhere.  A "functional interface" is simply an interface that defines a single method.   This associated interface is related to the lambda function only in that it is the "target" type to which the lambda is being assigned, either by variable assignment or by passing the lambda as an input parameter or output parameter of a method which is typed to that interface.

While the associated functional interface explicitly defines the types of the input parameters and output parameter (generics can be used), the declaration of a lambda function does not require that these types be explicitly defined, though they can be, if desired.  Rather, the types are inferred by the context in which the lambda function will be used.  In particular, when the lambda object is being assigned to a variable, that variable must be typed to be a particular interface and thus, typing of the lambda object can be inferred from that target interface.   When a lambda object is being passed as an input parameter to a method, it is beign assigned to one of the input parameter variables, and thus, the typing of the lambda can once again be inferred. 

In general, the syntax of a lambda function is a parentheses-enclosed, comma-separated list of input parameters, followed by an arrow, '->', and then either a single line expression with no 'return' call or multiple statement lines enclosed by curly braces, '{...}', including a return statement if a return value is required.  void returns are handled automatically with no special syntax.

Including the "@FunctionalInterface" annotation will enable the compiler to generate an error if the interface is ever modified so as not to be a functional interface any more.  Doing so would immediately break any code that utilizes the interface in a lambda expression, so this is a nice safety feature.  (See the Oracle docs on Annotation Type FunctionalInterface)

 

/**
 * A functional interface defining a single method, apply()
 */
@FunctionalInterface
public interface ILambdaTest1<T, R> {
	R apply(T x);
} 

// Using a lambda to intantiate an ILambdaTest1 instance.  3 equivalent syntaxes.

ILambdaTest1<Integer, String> lt_1 = (x) -> "x^2 = "+(x*x);    // semicolon at end is NOT part of lambda declaration, it is the end of the variable declaration!

ILambdaTest1<Integer, String> lt_2 = (int x) -> "x^2 = "+(x*x);	 // semicolon at end is NOT part of lambda declaration, it is the end of the variable declaration!

ILambdaTest1<Integer, String> lt_3 = (x) -> {return "x^2 = "+(x*x);}; // Note that all lines within the curly braces require semicolons as any normal Java statement does.

/**
 * A class that has a method that uses an ILambdaTest1 instance
 */
public class TestClass {
	
	
	public void greet(String name, ILambdaTest1<String, String> greetFn) {
		System.out.println(greetFn.apply(name));
	}
	
	/**
	 * A method that takes an ILambdaTest1 as one of its input parameters
	 */
	public void printFact(int val, ILambdaTest1<Integer, Integer> factFn) {
		System.out.println(val+"! = "+factFn.apply(val));
	}
}

tc = new TestClass();  // for use below

// Here's a call to the TestClass.greet() using a lambda with a single expression that evaluates to a string:
tc.greet("CS Student", (s) -> "Howdy "+s+"! Welcome to Rice!");

// Here's a call to the TestClass.printFact() using a lambda with multiple statements:
tc.printFact(5, (x) -> {   int result = 1;
                            int step = -x/Math.abs(x);
                            for(int i=x; 0!=i; i+=step) {
                                result *= i;
                            }
                            return result;
                         }); 


/**
 * A functional interface defining a single method, execute() that takes two input parameters
 */
public interface ILambdaTest2 {
	void execute(String str, double dval);
}

// A declaration of a lambda with multiple input parameters:
ILambdaTest2 il2 = (s, x)  -> s+": "+Math.sqrt(x)

Java 8 provides a number of handy pre-defined generic functional interfaces in the java.util.function package.   These interfaces that will save you the time and trouble of defining your own.

 

Closures and Lambda Functions

Lambda functions have the same closure capabilities as inner and nested classes, that is, that they close over whatever is in scope at the time and place when they are created.  The same restriction applies on local variables unfortunately, namely that local variables must be final or "effectively final" which means "non-mutated", in order to be referenced by the lambda function.

this is not this!:  One unexpected behavior is that "this" inside of the body of a lambda expression does NOT refer to the lambda function instance.   Instead, this refers to the nearest enclosing object instancewhich is consistent Java's with lexical scoping rules.    A lambda function is a stateless instantatiation of a function interface because the lambda expression syntax does not allow for the declaration of instance variables (fields).   Therefore, there is no statically-defined data and no other methods to be accessed, negating the need for a self-reference.    However, this does leave out the ability to self-reference when creating recursive operations.   To create a recursive lambda function, you must assign the lambda function to a variable and then the recursive call references that variable.   For a better solution, use a Y-combinator.

Comparing Lambda functions vs. anonymous inner classes:

Lambda Functions Anonymous Inner Classes
Closures: Closes over a single functional process Closes over multiple functional processes (methods), including instance fields of the anonynmous inner class.
  Local variables in closure must be final or "effectively final", i.e. not mutated. Local variables in closure must be final.
  "this" refers to the nearest enclosing object instance. "this" refers to the anonymous inner class instance.
Typing: Inferred typing, defined by the target interface at the point of usage. Explicitly typed, defined by superclass interfaces or classes.
     

Method References

Reference: Oracle documentation on Method References

A method of a class is really nothing more than a lambda function that closes over the object instance made from that class.  Conversely, an object contains a collection of lambda functions, i.e. its methods, all with the same closure.  This means that one should be able to use an object's method anywhere that a lambda function is used, provided its signature is appropriate for that usage.

In general the syntax for referencing a method as a lambda function is:  ClassName::methodName or instanceName::methodName

There are four distinct scenarios that encounters when using method references (see the above references for more detailed examples):

1. Referencing an instance method of a particular object:

In this scenario, the desired lambda function is the method of a particular object, probably because you want the function to be closing over the data and behaviors of that object.   This is probably the most common scenario.

Syntax:   instanceName::methodName

2. Referencing a constructor:

In this scenario, the desired lambda is a factory function, used to instantiate a new instance of a specified class.  

Syntax:  ClassName::new

3. Referencing an instance method of an unspecified object of a particular type:

In this scenario, the desired lambda is a specific method to be called on an arbitrary object of some specified type.  For instance, this is the scenario where you want to call a specific method on every element in a collection of objects but which method is not known until runtime.

Syntax:  ClassName::methodName

The tricky part of this scenario however is that if the desired method takes N parameters, there are actually N+1 variables; the object whose method is being called is also a variable here since it changes from call to call.   This means that the functional interface to which the lambda is associated must take N+1 input parameters, not just the N parameters of the specified method of the specified class in the method reference.   The first input parameter of the N+1 parameters in the associated functional interface must be the object whose method is to be called. 

/**
 * The functional interface for calling a method on a caller object of type A
 * with a parameter of type B.
 */ 
public interface ILambda<A, B, R> {
	R apply(<A> caller, <B> param); 

}


// a method that uses the above functional interface:
public ArrayList<R> mapFn(ArrayList<A> collectionOfA, B paramOfB, ILambda<A, B, R> lambdaFn) {
	ArrayList<R> = result;
	for(A anElement:collectionOfA){
		result.add(lambdaFn.apply(anElement, paramOfB));	
	}
	return result;
}

// Consider the charAtmethod of a String:

public  char charAt(int index) {...}

// Here's how one could extract the character from a specific index from every string in an array of strings: 
ArrayList<String>  arrayOfStrings = new ArrayList<String>();
// code to fill the array list elided...
int index = 42;  
ArrayList<Character> arrayOfChars = obj.mapFn(arrayOfStrings, index, String::charAt);  // arrayOfChars will hold the 42'nd character of every string in the given arrayOfStrings.

Those who know Python should recognize this extra parameter as the "self" parameter that must always be the first parameter of any method declaration.   Under the hood, Java also has this extra input parameter but does not force the programmer to explicitly declare it.   Thus, N+1 input parameters declared by the associated functional interface is really just the real underlying method signature of the desired method.

4. Referencing a static method:

In this scenario, the desired lambda is a static method of a class.

Syntax: ClassName::methodName

 

Addtional Resources

© 2020 by Stephen Wong