Kotlin is `fun` - Function types, lambdas, and higher-order functions
Lucy Linder
Posted on March 13, 2023
Kotlin treats functions as first-class citizens.
First-class means functions can be stored in variables and data structures and can be passed as arguments to and returned from other functions (higher-order functions). This is one of the idioms of functional programming, a programming style that I love.
To better understand what it means, and why it is so cool, let's go through the theory: what are function types, why they matter, and how they relate to lambdas and higher-order functions.
Did I lose you? Then read on! Everything will become clear, I promise! 😊
In this part:
🔖 I created this Table of Contents using BitDownToc. If you are curious, read my article: Finally a clean and easy way to add Table of Contents to dev.to articles 🤩
Function types
A function type is a special notation Kotlin uses to represent a function - basically its signature - that can be used to declare a function (e.g. as a parameter to another function) or as the type of a variable holding a function reference. It looks like this:
(TypeArg1, TypeArg2, ...) -> ReturnType
You put the type(s) of the parameter(s) in the left-side parenthesis and the return type on the right side. A function type that doesn't return anything must use the return type Unit
. For example:
- a function without arguments that doesn't return anything:
() -> Unit
- a function with two string arguments that returns a boolean:
(String, String) -> Boolean
Now, can you guess this one?
(String) -> (Int) -> Boolean
This looks confusing, right? It would be clearer if I rewrite it like this (this is equivalent):
(String) -> ((Int) -> Boolean)
This function type simply denotes a function that takes a String
as a parameter and returns another function, this time taking an Int
as a parameter and returning a Boolean
.
Instantiating function types
Function types can be instantiated in many ways.
Let's take a variable declared with the following function type: (String) -> Boolean
(something taking a String
as an argument, and returning a Boolean
). To initialize such a variable, we could use:
-
a lambda expression (often just called a lambda), expressed with curly braces:
// lambda with explicit type val lambdaE: (String) -> Boolean = { s -> s != null } // lambda with explicit type and implicit parameter val lambdaE: (String) -> Boolean = { it != null } // lambda with implicit type // (as anything can be null, s's type must be declared explicitly) val lambdaI = { s: String -> s != null }
-
an anonymous function, that is a function without any explicit name:
val anon = fun(s: String): Boolean = s != null
-
a reference to an existing function:
// a "normal" function fun isNotNull(s: String): Boolean = s != null // a ref to the function above val ref1 = ::isNotNull // a ref to an existing String function val ref2 = String::isNotBlank
Note: lambdas and anonymous functions are known as function literals - functions that are not declared but are passed immediately as an expression.
Once a function type is instantiated, it can be called (=invoked), or passed around, for example to other functions.
Invoking a function type
A function type can be called using its method invoke
, or more conveniently using the famous ()
:
val printHelloLambda = { println("hello !") }
printHelloLambda()
printHelloLambda.invoke()
Those calls can even be chained. For example:
// hint: (String) -> ((String) -> Unit)
val doubleLambda: (String) -> (String) -> Unit = { s1 ->
{ s2 -> println("$s1, $s2!") }
}
doubleLambda("Hello")("World") // prints "Hello, World!"
In short, however the function type has been instantiated, it behaves like a regular function.
Higher order functions
Higher order functions are just functions that have one or more parameters and/or return types that are function types. In other words, they are functions that receive other functions as parameters or return other functions.
Here is a useless higher-order function:
// a simple function
fun myName(): String = "derlin"
// a higher order function
fun higherOrderFunc(getName: () -> String) {
println("name is " + getName())
}
higherOrderFunc(::myName) // -> "My name is derlin"
Let's use a more interesting example. The following is a higher-order function that filters items from a list based on a condition (passed as a parameter) and also prints the items that were dropped to stdout:
fun evinceAndPrint(
lst: List<String>,
condition: (String) -> Boolean
): List<String> {
val (keep, drop) = lst.partition { condition(it) }
println("Evinced items: $drop")
return keep
}
This function could work on any kind of list, not just strings, so it would better be generic.
fun <T> evinceAndPrint(
lst: List<T>,
condition: (T) -> Boolean
): List<T> {
val (keep, drop) = lst.partition { condition(it) }
println("Evinced items: $drop")
return keep
}
Here is how we could use it:
val mixedCase = listOf("hello", "FOO", "world", "bAr")
evinceAndPrint(mixedCase, { s -> s == s.lowercase() })
// > Evinced items: [FOO, bAr]
Note, however, that this syntax is heavy (and ugly!). Fortunately, we can make it better. If you are using IntelliJ IDE, you should get two suggestions:
-
move the trailing lambda out of the parentheses:
According to Kotlin convention, if the last parameter of a function is a function, then a lambda expression passed as the corresponding argument
canshould be placed outside the parentheses -
Use the implicit name
it
for the single parameterIf the compiler can parse the signature without any parameters, the parameter does not need to be declared and
->
can be omitted. The parameter will be implicitly declared under the nameit
.
The call can then be rewritten as:
evinceAndPrint(mixedCase) { it == it.lowercase() }
And this is how you end up with so many constructs like:
listOf(1, 2, 3)
.filter { it % 2 == 0 }
.forEach { println(it) }
filter
and forEach
are simply higher-order functions with a single parameter and a trailing lambda! This is way easier to read than:
listOf(1, 2, 3)
.filter (fun(i: Int): Boolean = i % 2 == 0)
.forEach ({ i -> println(i) })
Bonus: function types under the hood
As explained in function-types.md, function types are implemented as interfaces:
package kotlin.jvm.functions
interface Function1<in P1, out R> : kotlin.Function<R> {
fun invoke(p1: P1): R // <- inherits from Function
}
These interfaces are named FunctionN
, where N
denotes the number of arguments. They all inherit from kotlin.Function
, which defines the invoke
method.
When you instantiate a function type (through lambdas or other means), you are thus actually creating an instance of one of those functional interfaces (thus callable from Java).
Why is this interesting to know? Well, as you may have guessed, the Kotlin team didn't write an infinity of those interfaces. They settled for 23, going from Function0
to Function22
. In other words, a function type (and thus a lambda) can only have up to 22 parameters.
Quiz time
Just for fun: what does this function do and how would you invoke (use) it? Try to give it a meaningful name.
fun <T, R, S> x(a: (T) -> R, b: (R) -> S): (T) -> S =
{ t: T -> b(a(t)) }
Here is an example: Written directly, answer
This function is the well-known function composition in the functional paradigm, which takes two functions A -> B
and B -> C
and returns a function A -> C
.
val trimAndParseInt = x(String::trim, String::toIntOrNull)
listOf("1 ", " asdf", " 100")
.mapNotNull(trimeAndParseInt)
.let(::println)
// > prints [1, 100]
trimAndParseInt
is equivalent to parse(trim(s))
:
// first argument
val trim: (String) -> String = String::trim
// second argument
val parse: (String) -> Int? = String::toIntOrNull
// what the x function body does
val trimAndParseInt: (String) -> Int? =
{ s -> parse(trim(s)) }
And that concludes our second article in the series! This was the most theoretical of all. Stay tuned to learn one of my favourite features of Kotlin: extension functions.
Posted on March 13, 2023
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.