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 SAM’s (Single-Abstract Method). Here is an example SAM 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 those two, the argument can be defined as:

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

Using function types with type aliases

When a function type gets complicated, we can name them using type aliases.

typealias OnClick = (View) -> Unit

A type alias provide another name for a type. Like a nickname. No matter if you use someone's full name or a nickname, you still mean the same person. The same with type aliases. During the compilation time, 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 an IDE. Although when we start naming parameters, the types tends 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

The functional interface is a heavier solution. Such interfaces needs to be defined, but in return:

  • they define a new named type,
  • handler functions can be named differently (in a function type it 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 adding non-abstract functions and implementing other interfaces.

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

When we design a class to be used from another language than Kotlin, interfaces are cleaner and better supported. Those other languages cannot see type aliases nor name suggestions. What is more, Kotlin function types when used from some languages (especially Java) require functions to return Unit explicitly:

// 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 function type is under the hood a generic type, primitives cannot be used. What 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 such a problem.

Overall, the main reasons to prefer functional interfaces are:

  • Java interoperability,
  • optimization for primitive types,
  • when we need to represent not merely a function, but needs to express expresses a concrete contract.

Functional interfaces were introduced in Kotlin 1.4. Currently, IntelliJ support for function types is better than for function 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 set them as separate properties holding 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 }

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 lambda expressions and all other options 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 Java SAM and Java function: since in such cases there is special support, and we can pass a function type instead.