5

Receivers and Extensions

 1 year ago
source link: https://typealias.com/start/kotlin-receivers-and-extensions/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

Standalone Functions and Object Functions

Way back in Chapter 2, we learned how to create functions. Here’s a very simple function that puts single quotes at the beginning and the end of a String:

fun singleQuoted(original: String) = "'$original'"

Listing 10.1

-

A simple function to wrap a string in single quotes.

As you recall, this function can be called easily, like this:

val title = "The Robots from Planet X3"
val quotedTitle = singleQuoted(title)

println(quotedTitle) // 'The Robots from Planet X3'

Listing 10.2

-

Calling a simple function.

And then in Chapter 4, we learned that objects can contain functions, too. For example, String objects have a function named uppercase() that returns the same string, but with all uppercased letters. You can call it like this:

val title = "The Robots from Planet X3"
val loudTitle = title.uppercase()

println(loudTitle) // THE ROBOTS FROM PLANET X3

Listing 10.3

-

Calling a function on an object.

When you call a function that’s on an object like this, you prefix the function call with the name of the object and a dot. For this reason, this way of writing a function call is called dot notation.

So, we have two different categories of functions here:

  • Functions that stand alone, apart from an object.
  • Functions that are called on an object.

The singleQuoted(title) function is called without prefixing an object name. The uppercase() function is called using a dot on a `String` object.No object name or dotObject name and dotsingleQuoted(title)title.uppercase()Calling a function onan objectCalling a standalonefunction

It’s easy to call a standalone function. It’s also easy to call a function on an object.

However, things become more difficult when you combine calls to these two different types of functions in one place. Take a look at this code, which calls one standalone function (singleQuoted()), and calls two functions with dot notation (removePrefix() and uppercase()).

singleQuoted(title.removePrefix("The ")).uppercase()

Listing 10.4

-

Calling three functions to change a string - two functions are on an object and another is a standalone function.

Can you figure out what order will these functions will be called in?

  1. First, removePrefix() is called.
  2. Then, the result from that call will be used as an argument to the singleQuoted() function.
  3. Finally, uppercase() will be called on the string object that is returned from singleQuoted().

Visually, our minds have to process this expression by bouncing around - starting in the middle, then moving to the left, then moving to the right.

The functions from the previous code listing are called in a different order than you read them.singleQuoted(title.removePrefix("The ")).uppercase()123

Imagine trying to read a book like this!

It's difficult to read text such as "fox jumps(the quick brown).over the lazy dog".fox jumps (the quick brown) over the lazy dog

It would be easier to read and understand the code if all three of these function calls worked the same way, so that we could read them in a single direction. For example, if you could use dot notation to call the singleQuoted() function - just like you do with removePrefix() and uppercase() - then it would be very easy to follow. Here’s what that would look like:

val newTitle = title.removePrefix("The ").singleQuoted().uppercase()

Listing 10.5

-

Calling three functions, each of them is a function on an object.

Since singleQuoted() isn’t a part of the String class, this code doesn’t actually work yet. But you can certainly see how much easier it is to read and understand it, because the functions are called in the same order that you read them. You can simply follow the code from left to right.

The code from the previous listing is easy to read, because it runs in the same order that you read it - left to right.title.removePrefix("The ").singleQuoted().uppercase()321

These calls could also be arranged vertically, one per line, like this:

val newTitle = title
    .removePrefix("The ")
    .singleQuoted()
    .uppercase()

Listing 10.6

-

Arranging a call chain vertically.

Again, it’s natural to read - from top to bottom.1

Besides making the function calls consistent and easy to read, there are times when dot notation just fits well with a Kotlin developer’s expectations. By convention, if a function primarily does something to an object or with an object, then we often expect that function to exist on the object.

Also, if you’re using an IDE like IntelliJ or Android Studio, functions that are “on an object” are easier to discover. If you’ve got a String object, and you wonder what functions can be called on it, just type the dot, and you’ll see a list of the available functions! This is a great way to explore classes that you’re not as familiar with.

Screenshot of IntelliJ showing the functions that can be called on a String object.

So, in this chapter, our goal is to change singleQuoted() so that it can be called with a dot, like this:

val newTitle = title.singleQuoted()

Listing 10.7

-

How we want the call site to look - calling singleQuoted() on an object instead of as a standalone function.

Let’s start by looking more closely at the similarities and differences between standalone functions, and those that are called on an object.

They’re Not So Different After All

These two categories of functions - standalone functions and functions that are called on an object - have more in common than you might think. Yes, the way that you have to write the code - that is, the syntax - to call the function is a little different in each case:

A standalone function on the left, and an object function on the right.singleQuoted(title)title.uppercase()

But in concept, they’re actually very similar:

  1. They both start off with a string.
  2. They both return a new string that is based on the original string.

From that standpoint, it’s almost as if each of these functions takes a String argument. The difference is only in where you put that argument when you call the function.

Both functions start with a string.singleQuoted(title)title.uppercase()title argumenttitle argument

When calling a function using a dot, the object to the left of the dot is called the receiver of the function call. Receivers are an important concept for this chapter, but they’re also important for understanding upcoming concepts like scope functions and more advanced lambdas, so let’s dig in!

Introduction to Receivers

A well-trained dog knows how to bark on command. When you tell your dog Fido to “speak”, you’re sending him a command, and he is the receiver of that command.

A sender, command, and receiver in real life.

Similarly, when you call a function on an object, that object is the receiver of that function call.

The call site is the sender, the function call is the command, and the receiver is still the object receiving the command.

Let’s flesh this out further with some code. Here’s a simple Dog class, followed by some code to tell the dog to speak.

class Dog {
    fun speak() {
        println("BARK!")
    }
}

val fido = Dog()
fido.speak()

Listing 10.8

-

A simple Dog class, with code telling it to speak.

Since fido is the dog you’re telling to speak(), fido is the receiver.

When calling `fido.speak()`, `fido` is the receiver.fido.speak()Receiver

Easy, right?

Now, sometimes your dog doesn’t need to be told to speak. Sometimes he will choose to bark on his own. (In fact, sometimes you can’t get him to stop barking… ask me how I know!)

Let’s update the Dog class so that Fido will bark whenever he starts playing.

class Dog {
    fun speak() {
        println("BARK!")
    }
    fun play() { 
        this.speak()
    }
}

Listing 10.9

-

Adding a play() function to the Dog class.

Here, the play() function calls the speak() function. As you might recall from Chapter 4, the keyword this refers to the same object that play() is called upon. In other words, if you call fido.play(), then speak() will be called on the fido object. In Listing 10.9, the receiver of the speak() function call is this.

You might also remember that you can omit this., so the following code works the same as the code in the previous listing.

class Dog {
    fun speak() {
        println("BARK!")
    }
    fun play() { 
        speak()
    }
}

Listing 10.10

-

Omitting this. when calling speak() from inside play().

Now, there’s no object name or dot before speak(); just the function name. Does this mean that there’s no receiver here?

Is there a receiver when calling `speak()` from inside `play()`?classDog {funspeak() {println("BARK!")}funplay() {speak()}}Any receiver here?

In fact, there is a receiver here! Remember - any time that a function is called on an object, that object is the receiver. Because speak() is being called on a Dog object, that object is the receiver. Inside the play() function, you can include this. before speak(), or you can omit it. The result is the same either way, and the receiver is the same either way.

So, speak() has a receiver here! It’s just not explicitly stated in the code. It’s implied. That’s why this is called an implicit receiver. Contrast this with the explicit receiver in Listing 10.8 above. The following shows two call sites for speak() - one that’s using an implicit receiver, and one that’s using an explicit receiver.

Two call sites for `speak()` - one using an implicit receiver and one using an explicit receiver.classDog {funspeak() {println("BARK!")}funplay() {speak()}}valfido =Dog()fido.speak()(Implicit Receiver)Explicit Receiver

By the way: Sometimes you need `this`

As mentioned above, you can often omit this, but there are times where it’s needed. If two functions have the same signature - that is, the same name, parameter types, and return type - then this can be helpful for choosing the right one. We’ll see more examples of this in the future.

Wow, that’s a lot of information about receivers, but we can summarize it like this:

  • A receiver is an object whose function you are calling.
  • It can either be explicit, as seen when calling a function with a dot, or…
  • It can be implicit, such as when one function calls another function inside the same class.

Now that we know about receivers, we can use this knowledge to get back to our original goal - updating the singleQuoted() function, so that we can call it with a dot.

Introduction to Extension Functions

As it’s currently written, the singleQuoted() function has a single parameter, called original, which is the string that will be wrapped with quotes. All we need to do now is to update the function so that it has a receiver instead of a normal function parameter.

Difference between the call site that we have currently and the call site that we want to have.title.singleQuoted()What we want:singleQuoted(title)What we have:

When you want to be able to call a function with a dot, one way to do this is to add the function to the class. However, you can’t always do this. The String class is part of the Kotlin standard library, so you can’t just open up its code and write a new function in it!

Thankfully, Kotlin provides a way to extend a class with your own functions, which can be called with a dot. These are called extension functions.

Let’s look at the singleQuoted() function that we wrote way back at the beginning of this chapter.

fun singleQuoted(original: String) = "'$original'"

Listing 10.11

-

The original function from [Listing #3174].

Let’s change the original parameter to be the receiver, so that singleQuoted() will be an extension function. It’s easy to do:

  1. First, prefix the function name with the type of the receiver that you want, and add a dot. In this case, we want a receiver that’s a String.
  2. Second, refer to the receiver using this inside the function body.

Here’s how singleQuoted() looks after making these changes:

fun String.singleQuoted() = "'$this'"

Listing 10.12

-

An extension function that does the same thing as the previous listing.

In this code:

  • String is the receiver type. By specifying this as String, you’ll be able to call singleQuoted() on any object that is a String.
  • this is the receiver parameter. It refers to whatever object singleQuoted() is called upon, so if you call title.singleQuoted(), then this will refer to the title object.

Breakdown of an extension function.Receiver TypeReceiver ParameterfunString.singleQuoted() ="'$this'"

You can easily convert a regular function to an extension function:

  1. Put the type of the parameter before the function name, and add a dot.
  2. Anywhere you used that parameter, rename it to this.
  3. Finally, remove the original parameter from between the parentheses.

Converting a standalone function to an extension function.funString.singleQuoted() ="'$this'"funsingleQuoted(original: String) ="'$original'"

If you’re using IntelliJ or Android Studio, you can also convert a function to an extension function by using the refactoring tools. To do this, right-click the function name, then choose “Refactor” and “Change Signature”. From there, checkmark the parameter that you want to convert to a receiver.

Using IntelliJ refactoring tools to convert a standalone function to an extension function.

With these changes, whenever you call this function, you must call it with a receiver, like this:

val quotedTitle = title.singleQuoted()

Listing 10.13

-

How to call singleQuoted(), now that it’s an extension function.

And now, it’s easy to insert this function call into the middle of a call chain:

val title = "The Robots from Planet X3"
val newTitle = title
    .removePrefix("The ")
    .singleQuoted()
    .uppercase()

// 'ROBOTS FROM PLANET X3'

Listing 10.14

-

Adding a call to singleQuoted() in the middle of a call chain.

Extension functions are quite common in Kotlin code. Kotlin’s standard library includes many extension functions, too. In fact, you might be surprised to learn that both removePrefix() and uppercase() are not actually members of the String class - they’re extension functions, too!

Extensions are a great way to give an existing type some new functionality, especially for classes where you can’t edit the class itself. Just keep in mind that extensions cannot access private members of a class. So, even though an extension function is called the same way as a member function, it doesn’t have access to all of the same things that a member function does!

Nullable Receiver Types

What happens when you want to call an extension function on a nullable object?

You’ll get an error message.

val title: String? = null
val newTitle = title.singleQuoted()

Listing 10.15

-

Error: "Only safe (?.) or non-null asserted (!!.) calls are allowed on a nullable receiver of type String?"

As you might remember from Chapter 6, you can work around this by using the safe-call operator ?. so that singleQuoted() is only called when title is not null.

val title: String? = null
val newTitle = title?.singleQuoted()

Listing 10.16

-

Using a safe-call operator when the object is nullable.

Kotlin also gives you another option, though - you can create an extension function that has a nullable receiver type. For example, instead of making the receiver type a non-nullable String, you can make it a nullable String?, like this:

fun String?.singleQuoted() =
    if (this == null) "(no value)" else "'$this'"

Listing 10.17

-

Creating an extension function for a nullable receiver type.

Inside this function, this is nullable. If this version of singleQuoted() is called on a null, then it returns a string that says (no value). Otherwise, it works like the previous version of singleQuoted(), as in Listing 10.12.

When an extension function has a nullable receiver type, you don’t have to call it with a safe-call operator. You can call it with a regular dot operator instead.

val title: String? = null
val newTitle = title.singleQuoted()

println(newTitle) // (no value)

Listing 10.18

-

Calling an extension function that has a nullable receiver type using a normal dot operator.

On the other hand, you could still choose to call it with the safe-call operator if you want, but in that case, the function will only be called if the receiver is not null. For example, the only difference between the following listing and the previous listing is that we changed from a regular dot operator to the safe-call operator. The result is that newTitle is null rather than (no value).

val title: String? = null
val newTitle = title?.singleQuoted()

println(newTitle) // null

Listing 10.19

-

Calling an extension function that has a nullable receiver type using a safe-call operator.

So, choose carefully between a dot operator and a safe-call operator, based on your expectations.

Extension Properties

In addition to extension functions, you can also create extension properties. However, you can’t use an extension property to actually store additional values inside a class. For example, it’s not possible to add an ID number to a String. Still, they can be helpful for making small calculations.

Let’s create an extension property that tells us if a String is longer than 20 characters.

val String.isLong: Boolean
    get() = this.length > 20

Listing 10.20

-

A simple extension property.

Just as with an extension function, an extension property specifies the receiver type, and the receiver parameter is available as this.

Breakdown of an extension property.Receiver TypeReceiver ParametervalString.isLong: Booleanget() =this.length>20

As mentioned before, when you’re calling a function or property on an implicit receiver, you don’t need to include this., so you could also write this property without it:

val String.isLong: Boolean
    get() = length > 20

Listing 10.21

-

Omitting this. inside the extension property.

You can use this property the same way as you’d use any property:

val string = "This string is long enough"
val isItLong = string.isLong

Listing 10.22

-

Using an extension property.

By the way: What about Context Receivers?

You might have recently come across the term context receiver.

Context receivers allow you to take one or more implicit receivers at a function call site, and make them available to the function that is being called. In practice, this means you can have a function that has multiple receiver parameters, rather than just one, as you have with extension functions.

As of right now, context receivers are a prototype feature, so it’s quite possible they’ll undergo some significant changes before they’re ready for use in real Kotlin projects. For that reason, I’m not covering them here yet. Many of us in the Kotlin community are excited about them, though!

Summary

In this chapter, you learned:

In the next chapter, we’ll learn about Scopes and Scope Functions. Kotlin developers use scope functions frequently, and in some cases, they can even be a helpful replacement for extension functions. See you then!

Thanks to David Blanc and Matt McKenna for reviewing this chapter.


  1. When the functions are called in the same order as you’d naturally read them (that is, left to right, top to bottom), developers often refer to this as a fluent interface. However, Martin Fowler and Eric Evans, who came up with that term, clarify that using chained function calls is only part of what makes an interface fluent. Read more thoughts about fluent interfaces from Martin Fowler. [return]

About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK