article banner (priority)

Function types

This is a chapter from the book Functional Kotlin. You can find it on LeanPub.

To represent functions as objects, we need a type to represent them. A type specifies what we can do with an object3, for instance by specifying what methods2 and properties it has. A function type is a type that specifies that an object needs to be a function. We can call this function using the invoke method. However, functions can have different parameters and result types, so there are many possible function types.

Defining function types

A function type starts with a bracket, inside which it specifies the parameter types, separated with commas. After the bracket, there must be an arrow (->) and the result type. Since all functions in Kotlin need to have a result type, a function that does not return anything significant should declare Unit1 as its result type.

Here are a few function types (in the next chapters, we will see them in use):

  • () -> Unit - the simplest function type, representing a function that expects no arguments and returns nothing significant5.
  • (Int) -> Unit - a function type representing a function that expects a single argument of type Int and returns nothing significant.
  • (String, String) -> Unit - a function type representing a function that expects two arguments of type String and returns nothing significant.
  • () -> User - a function type representing a function that expects no arguments and returns an object of type User.
  • (String, String) -> String - a function type representing a function that expects two arguments of type String and returns an object of type String.
  • (String) -> Name - a function type representing a function that expects a single argument of type String and returns an object of type Name.

Functions that return Boolean, like (T) -> Boolean, are often named predicate. Functions that transform one value to another, like (T) -> R, are often called transformation. Functions that return Unit, like (T) -> Unit, are often called operation.

Using function types

A function type offers only one method: invoke. Its parameters and result type are the same as defined by the function type.

fun fetchText( onSuccess: (String) -> Unit, onFailure: (Throwable) -> Boolean ) { // ... onSuccess.invoke("Some text") // returns Unit // or val handled: Boolean = onFailure.invoke(Error("Some error")) }

Since invoke is an operator4, we can "call an object" that has this method. This is an implicit invoke call.

fun fetchText( onSuccess: (String) -> Unit, onFailure: (Throwable) -> Boolean ) { // ... onSuccess("Some text") // returns Unit // or val handled: Boolean = onFailure(Error("Some error")) }

You can decide for yourself what approach you prefer. Explicit invoke calls are more readable for less experienced developers. An implicit call is shorter and, from a conceptual perspective, it better represents calling an object.

If a function type is nullable (in such a case, wrap it with a bracket and add a question mark at the end), you can use a safe call only with an explicit invoke.

fun someOperations( onStart: (() -> Unit)? = null, onCompletion: (() -> Unit)? = null, ) { onStart?.invoke() // ... onCompletion?.invoke() }

A function type can be used wherever a type is expected. For example, in a class definition, a generic type argument, or a parameter definition.

class Button(val text: String, val onClick: () -> Unit) var listeners: List<(Action) -> Unit> = emptyList() fun setListener(listener: (Action) -> Unit) { listeners = listeners + listener }

A function type can also be used as part of a function type definition.

  • (() -> Unit) -> Unit - a function type representing a function that expects a function type () -> Unit as an argument and returns nothing significant.
  • () -> () -> Unit - a function type representing a function that expects no arguments and returns a function type () -> Unit.

It is good to understand that a function type can include a function type, even though such function types are rarely useful.

Named parameters

Imagine a function type that expects many parameters, but it is unclear what every parameter means.

fun setListItemListener( listener: (Int, Int, View, View) -> Unit ) { listeners = listeners + listener }

A user of such a function will likely be confused, and automatic name suggestions are not helpful at all.

That is why function types can suggest parameter names. We place a name before a parameter type and separate them with a colon from other declared parameters.

fun setListItemListener( listener: ( position: Int, id: Int, child: View, parent: View ) -> Unit ) { listeners = listeners + listener }

Such names are visible in IntelliJ hints, and they have suggested names when we define a lambda expression for this type.

Named parameters are only for developers’ convenience, but they are not necessary from the technical point of view. However, it is good practice to add them if the parameters' meaning is unclear.

Type aliases

Function types can be long, especially when we use named arguments. In general, long types can be problematic, especially if they are repeated. Think of the setListItemListener example, where the same function type is repeated in the listener property and the removeListItemListener function.

private var listeners = emptyList<(Int, Int, View, View) -> Unit>() fun setListItemListener( listener: ( position: Int, id: Int, View, parent: View ) -> Unit ) { listeners = listeners + listener } fun removeListItemListener( listener: (Int, Int, View, View) -> Unit ) { listeners = listeners - listener }

We define a type alias with the typealias keyword. We then specify a name, followed by the equals sign (=), and we then specify which type should stand behind this name. Defining a type alias is just like giving someone a nickname. It is not a new type but a new way to reference the same already existing type. Both types can be used interchangeably because types generated with type aliases are replaced with their definitions during compilation.

typealias Users = List<User> fun updateUsers(users: Users) {} // during compilation it becomes // fun updateUsers(users: List<User>) {} fun main() { val users: Users = emptyList() // during compilation it becomes // val users: List<User> = emptyList() val newUsers: List<User> = emptyList() updateUsers(newUsers) // acceptable }

We define a type alias with the typealias keyword. We then specify a name, followed by the equals sign (=), and we then specify which type should stand behind this name. Defining a type alias is like giving someone a nickname. It is not really a new type: it’s just a new way to reference the same type. Both types can be used interchangeably because types generated with type aliases are replaced with their definitions during compilation.

typealias Users = List<User> fun updateUsers(users: Users) {} // during compilation becomes // fun updateUsers(users: List<User>) {} fun main() { val users: Users = emptyList() // during compilation becomes // val users: List<User> = emptyList() val newUsers: List<User> = emptyList() updateUsers(newUsers) // acceptable }

Type aliases can help us resolve name conflicts across libraries. For example, instead of the following code7:

import thirdparty.Name class Foo { val name1: Name val name2: my.Name }

We could use a type alias:

import my.Name typealias ThirdPartyName = thirdparty.name class Foo { val name1: ThirdPartyName val name2: Name }

Be careful because type aliases do not protect our types from misuse. If you define different names for the same type, they can all be used interchangeably6.

// DON'T DO THAT! Misleading and false type safety typealias Minutes = Int typealias Seconds = Int fun decideAboutTime(): Minutes = 10 fun setupTimer(time: Seconds) { /*...*/ } fun main() { val time = decideAboutTime() setupTimer(time) }

A function type is an interface

Under the hood, all function types are just interfaces with generic type parameters. This is why a class can implement a function type.

class OnClick : (Int) -> Unit { override fun invoke(viewId: Int) { // ... } } fun setListener(l: (Int) -> Unit) { /*...*/ } fun main() { val onClick = OnClick() setListener(onClick) }

We have learned something about function types, but we still do not know how to create objects of these types. This is what the following four chapters will be about, and we will start with the way that is the simplest, the oldest, and at the same time, the most forgotten: anonymous functions.

1:

Unit is an object with a single value that can be used within generic types. A function with the return type Unit is equivalent to a Java method that declares void.

2:

A method is a function associated with a class; it is called on an object, so both member and extension functions are methods.

3:

More about types in Kotlin for developers: Essentials, Typing system chapter.

4:

More about operators in Kotlin for developers: Essentials, Operators chapter.

5:

Those who have read the Typing system chapter from Kotlin for developers: Essentials might have guessed why I describe Unit as "nothing significant" instead of "nothing". Functions in Kotlin can indeed return nothing; in such cases, they declare Nothing as a result type, but this has a very different meaning than Unit.

6:

Protecting ourselves from type misuse is better described in Effective Kotlin, Item 49: Consider using inline value classes.

7:

The example was proposed by Endre Deak.