Function types
This is a chapter from the book Functional Kotlin. You can find it on LeanPub or Amazon. It is also available as a course.
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 Unit
1 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 typeInt
and returns nothing significant.(String, String) -> Unit
- a function type representing a function that expects two arguments of typeString
and returns nothing significant.() -> User
- a function type representing a function that expects no arguments and returns an object of typeUser
.(String, String) -> String
- a function type representing a function that expects two arguments of typeString
and returns an object of typeString
.(String) -> Name
- a function type representing a function that expects a single argument of typeString
and returns an object of typeName
.
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.
Since invoke
is an operator4, we can "call an object" that has this method. This is an implicit invoke
call.
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
.
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.
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.
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. We also use a colon between name and type.
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.
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.
Type aliases can help us resolve name conflicts across libraries. For example, instead of the following code7:
We could use a type alias:
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.
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.
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.
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.
A method is a function associated with a class; it is called on an object, so both member and extension functions are methods.
More about types in Kotlin Essentials, Typing system chapter.
More about operators in Kotlin Essentials, Operators chapter.
Those who have read the Typing system chapter from Kotlin 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
.
Protecting ourselves from type misuse is better described in Effective Kotlin, Item 52: Consider using inline value classes.
The example was proposed by Endre Deak.