Kotlin is `fun` - Function types, lambdas, and higher-order functions

Lucy Linder - Mar 13 '23 - - Dev Community

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

This looks confusing, right? It would be clearer if I rewrite it like this (this is equivalent):

(String) -> ((Int) -> Boolean)
Enter fullscreen mode Exit fullscreen mode

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()
Enter fullscreen mode Exit fullscreen mode

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!"
Enter fullscreen mode Exit fullscreen mode

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"
Enter fullscreen mode Exit fullscreen mode

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
}
Enter fullscreen mode Exit fullscreen mode

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
}
Enter fullscreen mode Exit fullscreen mode

Here is how we could use it:

val mixedCase = listOf("hello", "FOO", "world", "bAr")
evinceAndPrint(mixedCase, { s -> s == s.lowercase() })
// > Evinced items: [FOO, bAr]
Enter fullscreen mode Exit fullscreen mode

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:

  1. 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 can should be placed outside the parentheses

  2. Use the implicit name it for the single parameter

    If 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 name it.

The call can then be rewritten as:

evinceAndPrint(mixedCase) { it == it.lowercase() }
Enter fullscreen mode Exit fullscreen mode

And this is how you end up with so many constructs like:

listOf(1, 2, 3)
  .filter { it % 2 == 0 }
  .forEach { println(it) }
Enter fullscreen mode Exit fullscreen mode

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) })
Enter fullscreen mode Exit fullscreen mode

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
}
Enter fullscreen mode Exit fullscreen mode

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)) }
Enter fullscreen mode Exit fullscreen mode

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.

Here is an example:

val trimAndParseInt = x(String::trim, String::toIntOrNull)

listOf("1 ", "  asdf", " 100")
   .mapNotNull(trimeAndParseInt)
   .let(::println)
// > prints [1, 100]
Enter fullscreen mode Exit fullscreen mode

Written directly, 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)) }
Enter fullscreen mode Exit fullscreen mode

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.

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .