Effective Kotlin Item 38: Use function types or functional interfaces to pass operations and actions
This is a chapter from the book Effective Kotlin. You can find it on LeanPub or Amazon.
Many languages do not have the concept of a function type. Instead, they use interfaces with a single method. Such interfaces are known as SAMs (Single-Abstract Method). Here is an example SAM that is used to pass information about what should happen when a view is clicked:
When a function expects a SAM, we must pass an instance of an object that implements this interface1.
However, Kotlin supports two mechanisms that give us more freedom:
- the function type,
- the functional interface.
When we use either of these two, the argument can be defined as:
- A lambda expression or an anonymous function.
- A function reference or a bounded function reference.
- Objects that implement the declared function type or a functional interface.
Both function types and functional interfaces allow the above usages, but in general we consider function types as the standard way to represent operations and actions as objects.
Using function types with type aliases
As we said already, function types are the standard way to represent operations and actions as objects. If you want to name a concrete function type, you can use a type alias.
A type alias provides another name for a type that is like a nickname. No matter whether you use someone's full name or their nickname, you still mean the same person. It’s the same with type aliases: during compilation they are replaced with the type they represent.
Type aliases can also be generic:
Function type parameters can be named. The advantage of naming them is that these names can then be suggested by default by the IDE. However, when we start naming parameters, the types tend to get longer. This is why we often use this feature together with type aliases.
Reasons to use functional interfaces
A functional interface is a heavier solution. Such interfaces need to be defined, but in return:
- they define a new named type,
- handler functions can be named differently (in a function type, handler name is always
invoke
), - interoperability with other languages is better.
Functional interfaces also allow non-abstract functions to be added and other interfaces to be implemented.
When we design a class to be used from a language other than Kotlin, interfaces are cleaner and better supported. These other languages cannot see type aliases or have name suggestions. What is more, Kotlin function types need to return something, at least Unit
, and returning Unit
must be explicit in Java:
// Java
CalendarView c=new CalendarView();
c.setOnDateClicked(date->Unit.INSTANCE);
c.setOnPageChanged(date->{});
Another advantage of functional interfaces is that they are not wrapping types. Since a function type is a generic type under the hood, primitives cannot be used. This means that a parameter of type Int
in Java will be interpreted as Integer
instead of int
. This can sometimes make a difference, as explained in Item 47: Avoid unnecessary object creation. Functional interfaces do not have this problem.
Overall, the main reasons to prefer functional interfaces are:
- Java interoperability,
- optimization for primitive types,
- to help us represent not merely a function but an interface with a contract.
When you don't need any of these, use function types instead of functional interfaces.
Avoid expressing actions using interfaces with multiple abstract methods
Another practice that can be observed among developers who switched to Kotlin from Java is expressing actions using interfaces with multiple abstract methods:
This pattern was popular in Java when functional interfaces weren't supported. I believe this is largely a result of laziness. From an API consumer’s point of view, it is better to define them as separate properties containing either function types or functional interfaces:
In this way, the implementations of onDateClicked
and onPageChanged
do not need to be tied together in an interface. Now, these functions may be changed independently, and we can use function literals (e.g., lambda expressions) to set them.
Summary
- To express behavior, prefer function types or functional interfaces instead of standard interfaces or abstract classes.
- Function types are used more often. When they are used multiple times or are getting too long, we hide them behind type aliases.
- Functional interfaces are preferred primarily for Java (or other languages) interoperability and for more complicated cases when what we want to express is more than just an arbitrary function.
Unless it is a Java SAM: in such cases, there is special support, and we can pass a function type instead.