Skip to content

Latest commit

 

History

History
574 lines (420 loc) · 25.5 KB

File metadata and controls

574 lines (420 loc) · 25.5 KB

Chapter 6: Functions, Functions, Functions!

Previously, we've covered Kotlin's first-class support for functions and function types. Now, let's dive even further into some of its capabilities related to functions.

Local functions

We've seen that we can have functions that exist outside of classes, as top-level declarations in a file - these functions exist on the package level. On the other extreme, functions can also exist in very small scopes: within another function. This can come in handy for quick helper functions that won't be used anywhere else in the application.

Let's take a function that's called with the first and last name for a user, and checks both of these for being null, blank (only whitespace), or containing digits. If either of these checks fail, the user should not be saved.

private fun saveUser(firstName: String?, lastName: String?) {
    if (firstName.isNullOrBlank() || firstName.any { it.isDigit()}) {
        throw IllegalArgumentException("Invalid input $firstName")
    }
    if (lastName.isNullOrBlank() || lastName.any { it.isDigit() }) {
        throw IllegalArgumentException("Invalid input $lastName")
    }
  
    println("Saving user $firstName $lastName...")
}

isNullOrBlank is one of the many handy String extensions from the Kotlin Standard Library. We'll take a look at more of them in the next chapter.

To avoid duplication, we can extract the validation logic into a function. However, since we only need to use this specific logic in one place in our application, this can be a local function, which will only be in scope within the enclosing function:

private fun saveUser(firstName: String?, lastName: String?) { 
    fun validate(name: String?) {
        if (name.isNullOrBlank() || name.any { it.isDigit() }) {
            throw IllegalArgumentException("Invalid input $name")
        }
    }

    validate(firstName)
    validate(lastName)

    println("Saving user $firstName $lastName...")
}

Note that this function also has access to the parameters and local variables of the enclosing function - it acts as a closure. For this reason, it can only be invoked after its declaration within the containing function.

Function literals with receivers

As stated before, Kotlin doesn't generally claim to have original features. Almost everything it does has been done in other languages before - though Kotlin does often improve on syntax and cohesion.

One feature that Kotlin does claim to have uniquely is function literals with receivers (or lambdas with receivers). This a powerful combination of two features that we've looked at before: lambdas and extension functions.

Not only can a function type in Kotlin have parameters and a return type, it can also have a receiver. For example, take the following, simple extension function from earlier:

fun String.lastChar(): Char {
    return this[this.length - 1]
}

If we create a reference to this, and assign it to a variable, we can type it like so:

val ref: String.() -> Char = String::lastChar

Notice that an extension function on a type can be referenced with the :: syntax, just like any real member of that type.

Similarly to how String. preceded the function name at its declaration to indicate that it's an extension on the String type, when declaring a function type that's an extension, the same String. syntax can show up before the parentheses of the parameter list.

Next, we'll look at two examples of higher order functions which make excellent use of this feature.

The StringBuilder example

First, let's take a look at an example that showcases that we can be placed in the scope of any object with such a lambda, even ones that we don't create ourselves.

StringBuilder is a well-known class from Java that allows us to assemble strings from pieces without having to create many intermediate String instances, which would then be thrown away, wasting allocations. The basic usage of its API looks like this:

// 1
val builder = StringBuilder()
// 2
builder.append("Little Timmy is ")
builder.append(age)
builder.append(" years old today")
// 3
val result = builder.toString()
println(result)

There's a pattern here, which is repeated every time a StringBuilder is used.

  1. We create an instance.
  2. We perform a set of actions on it (various append calls).
  3. Finally, we call toString on it, to fetch the String that we've built.

Let's extract this to a higher order function, using lambdas with receivers:

inline fun buildString(actions: StringBuilder.() -> Unit): String {
    val builder = StringBuilder() // 1
    builder.actions()             // 2
    return builder.toString()     // 3
}

This function receives a lambda as a parameter, which itself is an extension on StringBuilder.

This is very similar to a lambda that receives a StringBuilder as a parameter, it just gets to access that reference as its receiver instead. Either way, it can operate on it.

Inside buildString, we perform the pattern pointed out earlier:

  1. We create a new instance of StringBuilder.
  2. We perform the actions on it - which we can do, as actions is an extension on StringBuilder.
  3. We extract and return the result stored in the builder.

The real magic, however, happens on the call site, which looks like this:

val result = buildString {
    this.append("Little Timmy is ")
    append(age)
    append(" years old today")
}
println(result)

Within the curly braces defining the lambda that we're passing in to buildString, we are in the body of an extension on the StringBuilder type. This means that this refers to some instance of a StringBuilder (in this case, this will be the one being created inside the buildString function). Methods on StringBuilder can also be called without the this qualifier in front of them, as if we were within the body of that class.

It's important to see that all we are doing is defining an extension function on StringBuilder (with a lambda, for the sake of this example), because that's what the buildString function requires as a parameter. After we pass this parameter in, we don't actually know what StringBuilder instance (or instances) this lambda will be called on, how many times, or at what time.

Being able to "set the scope" of a lambda to any object so that it can operate on it is an immensely powerful language feature, and we'll see it used for various purposes.

The database example

Another classic example of using a function literal with a receiver, as well as some other functional language features, is that of a database transaction (as presented by Jake Wharton in this talk). Let's say we have the following database interface:

interface Database {
    fun beginTransaction()
    fun commitTransaction()
    fun rollbackTransaction()
    fun insert(item: Int)
    fun delete(item: Int)
}

We'll assume that insert and delete might throw exceptions (IllegalStateException, specifically) if the operations fail. Every modification has to happen inside a transaction, which can be started with beginTransaction, and then either cancelled with rollbackTransaction in the case of an error, or completed with commitTransaction if everything went well.

With that, interacting with this database will look like this:

db.beginTransaction()
try {
    db.insert(11)
    db.delete(12)
    db.commitTransaction()
} catch (e: IllegalStateException) {
    db.rollbackTransaction()
}

We'd ideally want to focus on just the one tiny bit of code in the middle that matters to us from the example above - the insert and delete operations. Everything else around them is boilerplate that we'll repeat every time we touch the database. We can easily introduce a higher order function that takes care of all the ceremony of handling transactions and exceptions for us, extracting just that inner code into a parameter:

fun inTransaction(db: Database, block: () -> Unit) {
    db.beginTransaction()
    try {
        block()
        db.commitTransaction()
    } catch (e: IllegalStateException) {
        db.rollbackTransaction()
    }
}

We can now use this function as clients, by passing in our two operations in a lambda:

inTransaction(db) {
    db.insert(11)
    db.delete(12)
}

Let's make use of extension functions and make our helper function an extension on Database, to make the call site even nicer:

fun Database.inTransaction(block: () -> Unit) {
    beginTransaction()
    try {
        block()
        commitTransaction()
    } catch (e: IllegalStateException) {
        rollbackTransaction()
    }
}

db.inTransaction {
    db.insert(11)
    db.delete(12)
}

Next, let's make the lambda we're passing in take the Database as a parameter, so that we can make sure that our block of code operates on the same instance that we've started the transaction on:

fun Database.inTransaction(block: (Database) -> Unit) {
    beginTransaction()
    try {
        block(this)
        commitTransaction()
    } catch (e: IllegalStateException) {
        rollbackTransaction()
    }
}

We now get the Database in our lambda as the single parameter, which means that it can be referred to as it by default:

db.inTransaction {
    it.insert(11)
    it.delete(12)
}

To put our newly acquired knowledge of lambdas with receivers to work, it's time for another improvement. Even better than giving the lambda supplied by our clients a Database as a parameter, we can put them in the scope of the Database that they'll operate on, by making the lambda parameter an extension on that type.

fun Database.inTransaction(block: Database.() -> Unit) {
    beginTransaction()
    try {
        this.block() // or just block()
        commitTransaction()
    } catch (e: IllegalStateException) {
        rollbackTransaction()
    }
}

With that, the call site is as clean as can be - inside the braces, we're writing code as if we were in the Database class. We're also still in a transaction.

db.inTransaction {
    insert(11)
    delete(12)
}

Of course, we mustn't forget performance when it comes to higher-order functions. Passing in a lambda would mean an object allocation, which we can avoid by making the method inline:

inline fun Database.inTransaction(block: Database.() -> Unit) { /* ... */ }

Having the code handling transactions and performing the try-catch inlined will result in bytecode that's the same as if we've written these structures around our database operations everywhere in our codebase where we perform them. However, at the source level, we don't have to implement it over and over again. We've abstracted away the logic for it into a reusable function, and we did this, yet again, for free.

Tail recursion

It's a relatively well-known fact computer science circles that anything that can be solved iteratively can also be solved with recursion, and vice versa. Many problems lend themselves naturally to the recursive approach, calculating factorials being one of them. The trivial recursive implementation of factorial looks like this:

fun fact(n: Int): Int {
    if (n == 0) 
        return 1
    return n * fact(n - 1)
}

We know that the factorial of 0 is by 1 by definition, and for any other value of n, we can multiply the factorial of n - 1 by n to compute the result.

This function works well enough - ignoring the upper bounds of the Int type for simplicity - but it builds a call stack which is as deep as the parameter that was passed in. Why is this call stack maintained? Because when the last call of fact in the stack returns 1, each level of the stack has to be visited backwards, so they can multiply the result calculated so far with their own n value, before returning it and removing their function call's frame from the stack.

There's another way to implement this same recursion, using an accumulator variable, res, which we'll expect callers to set to 1 initially. Inside the function, if we've reached the end of the recursion, we return whatever value is in res. Otherwise, we go a level deeper, decrementing n, and performing the multiplication on res.

fun fact(n: Int, res: Int): Int {
    if (n == 0)
        return res
    return fact(n - 1, res * n)
}

This means that instead of doing the multiplication going upwards, while unwinding the stack, we're instead performing it on the way down to the bottom. When we reach the bottom, we already have our final value computed. At this point, all the intermediate steps of walking back through the stack are unnecessary. Nothing happens in those intermediate functions anymore, they all just return what they've received from a level down. We don't really need the call stack here!

This is a well-known pattern called tail recursion. A function is tail-recursive if all of its recursive calls to itself are the very last call in the function (in a given execution branch, that is), and if the result of that recursive call is returned as-is, without any additional operations being performed.

In such functions, the recursive calls and building the call stack can be optimized away, so that the recursion is replaced with an iterative solution at compile time. The trick is simple: instead of performing a recursive call at the tail of the function, simply rewrite the values of the function's parameter with those values that are in the recursive call's parameter list (in the current call frame), and then jump back to the beginning of the function to execute it again! This is semantically the same as performing the recursive call, but it doesn't deepen the stack.

Some compilers perform this optimization automatically when they notice that a function is tail recursive. Kotlin, however, chooses to be explicit about this. You have to mark your function with the tailrec keyword for the optimization to kick in:

tailrec fun fact(n: Int, res: Int): Int {
    if (n == 0)
        return res
    return fact(n - 1, res * n)
}

Why? Mostly to make sure that whatever you want optimized is truly eligible for the optimization. If you mark a function that's not truly tail recursive with the keyword, you'll get a warning.

This tail-recursive implementation comes with the downside of requiring callers to pass in an extra 1 as a parameter, which is a bit odd. This can be easily fixed by adding a one-parameter helper function for clients to use (and the recursive function could then be marked private):

fun fact(n: Int) = fact(n, 1)

The Standard Library's scoping functions

The Standard Library contains a small suite of very simple functions that perform basic "scoping" operations, using features we've learned about: higher order functions and function literals with receivers.

These are used so frequently in Kotlin that they are often seen as language features - but it's important to see that these are not special whatsoever, and you could implement any of them in a minute. They, as many other constructs in the language, are just functions.

The let function

Let's start with let. let is defined as an extension on a generic T type, which means you can invoke it on any value. It takes a lambda as its parameter, which accepts the same T type, and returns some R type.

This all sounds rather abstract, so let's take a look at the implementation:

public inline fun <T, R> T.let(block: (T) -> R): R {
    return block(this)
}

What does let do, in simple terms? It executes the piece of code you pass to it (usually as a lambda). Your lambda will be called with the receiver of let as its parameter. Whatever you return from the lambda will also be the return value of the let call.

Take this example:

val length = File("./README.md").let {
    println(it.name)
    println(it.absoluteFile)
    println(it.length())
    it.length()
}

Inside the lambda, it will refer to the File instance, as that's what you've called it on - that's the receiver of let. The last expression of the lambda will be the return value of the entire let call.

Remember the map function? You can think of let as a map operation on a single element.

You can use let instead of creating a local variable to introduce a temporary name for an object, if you give the lambda's incoming parameter an explicit name. Take a look at this usage of let:

File("./README.md").let { file ->
    println(file.name)
    println(file.length())
    println(file.absoluteFile)
}

Since let is inline, this is exactly equivalent* to the following code, which creates a regular local variable, with no extra cost at runtime (no actual lambda allocation, for example).

*except let also contains the variable in its own scope

val file = File("./README.md")
println(file.name)
println(file.length())
println(file.absoluteFile)

A popular use case for let is using it to perform null checks. Since there's a function invocation here (not at runtime, due to inlining, but semantically and on the source level) right before the actions are executed on the receiver of let, a safe call can be injected here:

val someFile: File? = ...
someFile?.let { file: File ->
    println(file.name)
}

If someFile happens to be null, the function call let will simply not be invoked by the safe call operator. If it is invoked, we get the receiver inside the lambda with a non-null type. This makes sense: if it was null, the lambda wouldn't be executing in the first place. This is a null check, which conditionally executes the code inside the lambda that we've passed in!

This null check is a versatile one. It can, for example, be used in the situation of dealing with a nullable, mutable property. We've seen before that these can not be null-checked and smart cast by regular means:

private var timer: AnimationTimer? = null

override fun start(primaryStage: Stage) {
    if (timer != null) {
        timer.stop()
        // ^ This line doesn't compile!
    }
}

The issue with this code was that the value of timer was being read again inside the body of the if statement, and its value might have been modified concurrently between those two reads, potentially to null.

This isn't an issue for the ?.let {} idiom, however. Take the following code:

private var timer: AnimationTimer? = null

override fun start(primaryStage: Stage) {
    timer?.let {
        it.stop()
    }
}

If timer is not null, its current value is passed into the let function and then back into the lambda as a parameter. If the value of the property changes in the meantime, that change won't be reflected in the reference that the lambda receives - that reference is held onto as a parameter value, and can not change.

This means the let call here is equivalent to this longer, more manual solution:

private var timer: AnimationTimer? = null

override fun start(primaryStage: Stage) {
   val t = timer
   if (t != null) {
       t.stop()
   }
}

In essence, we are using let to very concisely create this local, temporary copy of the value stored in the property.

The apply function

Along with let, apply is one of the most used scoping functions in Kotlin.

apply can also be called on any generic type, and it also takes a lambda as its parameter. This lambda is an extension on the type that apply was called on, and apply invokes the lambda on its receiver. In other words, the lambda is applied to the object. Finally, apply returns the original object.

Here's all of that in code:

public inline fun <T> T.apply(block: T.() -> Unit): T {
    this.block()
    return this
}

Let's see a real-life use case. Take a Rectangle class, which only has a no-param constructor. If you want to create an instance and initialize it, you'll end up writing code like this:

val rect = Rectangle()
rect.x = 8.0
rect.y = 9.0
rect.width = 100.0
rect.height = 200.0

Using apply, you can move your code into the scope of that Rectangle, as its parameter will be an extension on the Rectangle type, and executed on the receiver. This lets you be a lot more concise with these assignments:

val rect = Rectangle().apply {
    x = 8.0
    y = 9.0
    width = 100.0
    height = 200.0
}

Remember our earlier buildString implementation?

inline fun buildString(actions: StringBuilder.() -> Unit): String {
    val builder = StringBuilder()
    builder.actions()
    return builder.toString()
}

We can now rewrite this using apply, to avoid having to create a local variable:

inline fun buildString(builderAction: StringBuilder.() -> Unit): String =
    StringBuilder().apply(builderAction).toString()

The buildString function is actually part of the Standard Library, with this exact implementation.

This is all that apply does: it executes an extension on its receiver, and then returns the original object. It's a quick way to open a lambda into the scope of an object, and is usually used at object creation and initialization, to group related operations together.

Also, run.

There are two more extensions that are more rarely used variations on these previous two.

also is similar to apply, as it returns its receiver. It also executes the lambda passed to it, but it passes in its receiver as a parameter (unlike apply, which exposed it to the lambda as a receiver).

public inline fun <T> T.also(block: (T) -> Unit): T {
    block(this)
    return this
}

Like apply, this is usually used around object creation, to perform side effects:

fun register(name: String): Person {
    return Person(name).also {
        log(it)
    }
}

This could also be done by using apply, it would simply change the syntax inside the lambda to log(this). In general, a good rule of thumb is that you shouldn't really need to reference this inside an apply call. If you find yourself doing that, consider using also instead.


run is similar to let, it returns the result of the lambda passed to it, performing a mapping operation. However, inside the lambda, it exposes its receiver as a receiver (unlike let, which uses a regular function parameter instead).

public inline fun <T, R> T.run(block: T.() -> R): R {
    return block()
}

Here's a handy chart to sum it all up:

Chart of scope functions

Finally, the with function deserves an honourable mention. It's just like run, as it places code into the scope of a given object. However, instead of being an extension, it's a regular function that takes the object it operates on as a parameter.

with(robot) {
    goForward()
    turnLeft()
    goForward()
    turnRight()
}

The use function

An excellent example of just how powerful Kotlin's features around functions are is its use function. This function is equivalent to the try-with-resources construct in Java, which automatically closes a given resource when the end of its block is reached:

try (PrintWriter writer = new PrintWriter(new File("test.txt"))) {
    writer.println("Hello World");
}

In Kotlin, we can replace this language construct with a simple call to a higher order function:

PrintWriter(File("test.txt")).use { writer -> 
    writer.println("Hello World") 
}

Here's a (simplified) version of the use function's implementation, to give you an idea of how it works:

public inline fun <T : Closeable?, R> T.use(block: (T) -> R): R {
    try {
        return block(this)
    } catch (e: Throwable) {
        throw e
    } finally {
        close()
    }
}

The real implementation has a lot more code than this - exceptions and Closeables are a complex topic.

Summary

Kotlin has a lot to offer when it comes to functions. It has functions in many scopes: top-level, member, and even local functions.

One of its most powerful functional features is function literals with receivers, which allows you to place the caller of your function into the scope of a specific type when they're defining a lambda.

The Standard Library offers several of its own scoping functions (building on lambdas with receivers, as well as higher order functions in general) that make simple, everyday tasks easier.

Sources