article banner

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:

interface OnClick { fun onClick(view: View) }

When a function expects a SAM, we must pass an instance of an object that implements this interface1.

fun setOnClickListener(listener: OnClick) { //... } setOnClickListener(object : OnClick { override fun onClick(view: View) { // ... } })

However, Kotlin supports two mechanisms that give us more freedom:

  • the function type,
  • the functional interface.
// Function type usage fun setOnClickListener(listener: (View) -> Unit) { //... } // Functional interface declaration fun interface OnClick { fun onClick(view: View) } // Functional interface usage fun setOnClickListener(listener: OnClick) { //... }

When we use either of these two, the argument can be defined as:

  • A lambda expression or an anonymous function.
setOnClickListener { /*...*/ } setOnClickListener(fun(view) { /*...*/ })
  • A function reference or a bounded function reference.
setOnClickListener(::println) setOnClickListener(this::showUsers)
  • Objects that implement the declared function type or a functional interface.
class ClickListener : (View) -> Unit { override fun invoke(view: View) { // ... } } setOnClickListener(ClickListener())

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.

typealias OnClick = (View) -> Unit

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:

typealias OnClick<T> = (T) -> Unit

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.

typealias OnClick = (view: View) -> Unit fun setOnClickListener(listener: OnClick) { /*...*/ }

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.
interface SwipeListener { fun onSwipe() } fun interface FlingListener { fun onFling() } fun setOnClickListener(listener: SwipeListener) { // when swipe happens listener.onSwipe() } fun main() { val onSwipe = SwipeListener { println("Swiped") } setOnClickListener(onSwipe) // Swiped val onFling = FlingListener { println("Touched") } setOnClickListener(onFling) // Error: Type mismatch }

Functional interfaces also allow non-abstract functions to be added and other interfaces to be implemented.

interface ElementListener<T> { fun invoke(element: T) } fun interface OnClick : ElementListener<View> { fun onClick(view: View) override fun invoke(element: View) { onClick(element) } }

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:

// Kotlin class CalendarView() { var onDateClicked: ((date: Date) -> Unit)? = null var onPageChanged: OnDateClicked? = null } fun interface OnDateClicked { fun onClick(date: Date) }
// 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:

class CalendarView { var listener: Listener? = null interface Listener { fun onDateClicked(date: Date) fun onPageChanged(date: Date) } }

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:

// Using functional interfaces class CalendarView { var onDateClicked: OnDateClicked? = null var onPageChanged: OnPageClicked? = null } // Using function types class CalendarView { var onDateClicked: ((date: Date) -> Unit)? = null var onPageChanged: ((date: Date) -> Unit)? = null }

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.
1:

Unless it is a Java SAM: in such cases, there is special support, and we can pass a function type instead.