Kotlin extensions can be used to add new functions and properties to existing classes—even to classes that you didn’t write! In this chapter, we’ll cover explicit receivers, implicit receivers, extension functions, and extension properties.
Buckle up!
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'"As you recall, this function can be called easily.
val title = "The Robots from Planet X3" val quotedTitle = singleQuoted(title) println(quotedTitle) // 'The Robots from Planet X3'And then in Chapter 4, we learned that objects can contain functions, too. For example,
Stringobjects have a function nameduppercase()that returns the same string, but with all uppercase letters.val title = "The Robots from Planet X3" val loudTitle = title.uppercase() println(loudTitle) // THE ROBOTS FROM PLANET X3When we call a function that’s on an object like this, we 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, there are two different categories of functions:
Functions that stand alone, apart from an object.
Functions that are called on an object.
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 we 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()anduppercase()).singleQuoted(title.removePrefix("The ")).uppercase()Can you figure out what order these functions will be called in?
First,
removePrefix()is called.Then, the result from that call will be used as an argument to the
singleQuoted()function.Finally,
uppercase()will be called on theStringobject that is returned fromsingleQuoted().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.
Imagine trying to read a book like this!
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 we could use dot notation to call the
singleQuoted()function—just like we do withremovePrefix()anduppercase()—then it would be very easy to follow. Here’s what that would look like.val newTitle = title.removePrefix("The ").singleQuoted().uppercase()Since
singleQuoted()isn’t a part of theStringclass, this code doesn’t actually work yet. But it’s clear to see how much easier this is to read and understand, because the functions are called in the same order that we would read them.We can simply follow the code from left to right.
These calls could also be arranged vertically, one per line, like this.
val newTitle = title .removePrefix("The ") .singleQuoted() .uppercase()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, when using an IDE like IntelliJ IDEA, functions that are “on an object” are easier to discover.
If you’ve got a
Stringobject, 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 familiar with!
So, in this chapter, our goal is to change
singleQuoted()so that it can be called with a dot.val newTitle = title.singleQuoted()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 we have to write the code—that is, the *syntax—to call the function is a little different in each case.
But in concept, they’re actually very similar. They both start off with a
Stringobject, and they both return a newStringobject that is based on the original. From that standpoint, it’s almost as if each of these functions takes aStringargument. The difference is only in where we put that argument when we call the function.When calling a function using a dot, the object to the left of the dot is known as 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.
Similarly, when you call a function on an object, that object is the receiver of that function call.
Let’s flesh this out further with some code. Here’s a simple Dog class with some code to tell the dog to speak.
class Dog { fun speak() { println("BARK!") } } val fido = Dog() fido.speak()Since
fidois the dog you’re telling tospeak(),fidois the receiver.Now, sometimes your dog doesn’t need to be told to speak. Sometimes he will choose to bark on his own. Let’s update the
Dogclass so that Fido will bark whenever he starts playing.class Dog { fun speak() { println("BARK!") } fun play() { this.speak() } }Here, the
play()function calls thespeak()function. The keywordthisrefers to the same object thatplay()is called upon. In other words, when we callfido.play(), thenspeak()will be called on thefidoobject. In Listing 10.9, the receiver of thespeak()function call isthis.We can also omit
"this."—so the following code works the same as the code above.class Dog { fun speak() { println("BARK!") } fun play() { speak() } }And now, there’s no object name or dot before
speak()—just the function name. Does this mean that there’s no 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 aDogobject, that object is the receiver. Inside theplay()function, we can include"this."beforespeak(), or we 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 it’s known as an implicit receiver.
Contrast that with the explicit receiver in Listing 10.8 above. The code to the right shows two call sites for
speak()—one that’s using an implicit receiver, and one that’s using an explicit receiver.Wow, that’s a lot of information about receivers, but we can summarize it in three points:
A receiver is an object whose function you are calling.
It can 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, namedoriginal, 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.When we 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, we can’t always do that. For example, the
Stringclass is part of the Kotlin standard library, so we can’t just open up its code and write a new function in it!Thankfully, Kotlin provides a way to extend a class with our 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'"Let’s change the original parameter to be the receiver, so that
singleQuoted()will be an extension function.
First, we prefix the function name with the type of the receiver that we want, and add a dot. In Listing 10.11 above, we want a receiver that’s a
String.Second, we refer to the receiver using
thisinside the function body.Here’s how
singleQuoted()looks after making these changes.fun String.singleQuoted() = "'$this'"In this code:
String
is the **receiver type**. By specifying it asString, we’ll be able to callsingleQuoted()on any object that is aString`.
thisis the receiver parameter. It refers to whatever objectsingleQuoted()is called upon, so if we calltitle.singleQuoted(), thenthiswill refer to thetitleobject.We can easily convert a regular function to an extension function. Here’s how we can do that.
Put the type of the parameter before the function name, and add a dot.
Anywhere that the parameter was used, rename it to
this.Finally, remove the original parameter from between the parentheses.
With these changes, whenever we call
singleQuoted(), we must call it with a receiver, as shown here.val quotedTitle = title.singleQuoted()And now, it’s easy to insert a call to
singleQuoted()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'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()anduppercase()are not actually members of theStringclass—they’re extension functions!Extensions are a great way to give an existing type some new functionality, especially for classes where we can’t edit the class itself. Just keep in mind that extensions cannot access
privatemembers 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 we want to call an extension function on a nullable object? We’ll get an error message.
val title: String? = null val newTitle = title.singleQuoted()ErrorAs you might remember from Chapter 6, we can work around it by using the safe-call operator so that
singleQuoted()is only called whentitleis not null.val title: String? = null val newTitle = title?.singleQuoted()Kotlin also gives us another option, though—we can create an extension function that has a nullable receiver type. For example, instead of making the receiver type a non-nullable
String, we can make it a nullableString?, as shown here.fun String?.singleQuoted() = if (this == null) "(no value)" else "'$this'"Inside that function,
thisis nullable. IfsingleQuoted()is called on a null, then it returns a string that says"(no value)". Otherwise, it works like the previous version ofsingleQuoted(), as in Listing 10.12.When an extension function has a nullable receiver type, we don’t have to call it with a safe-call operator. We can call it with a regular dot operator instead.
val title: String? = null val newTitle = title.singleQuoted() println(newTitle) // (no value)On the other hand, we could still choose to call it with the safe-call operator if we 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
newTitleisnullrather than"(no value)".val title: String? = null val newTitle = title?.singleQuoted() println(newTitle) // nullSo, choose carefully between a dot operator and a safe-call operator, based on your expectations.
Extension Properties
In addition to extension functions, we can also create extension properties. However, we 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
Stringis longer than 20 characters.val String.isLong: Boolean get() = this.length > 20Just as with an extension function, an extension property specifies the receiver type, and the receiver parameter is available as
this.As mentioned before, when calling a function or property on an implicit receiver, we don’t need to include
"this."so we could also writeisLongwithout it.val String.isLong: Boolean get() = length > 20We can use this property the same way as we’d use any property.
val string = "This string is long enough" val isItLong = string.isLongNow you know how to create both extension functions and extension properties!
Summary
With receivers and extensions in your playbook, you can add functions and properties to types, even when you don’t have access to their source code!
Here’s what you tackled in this chapter:
- The difference between standalone functions and object functions.
- All about explicit and implicit receivers.
- How to create an extension function.
- How to create an extension function that has a nullable receiver type.
- How to create an extension property.
Are you ready for the next play? Huddle up for scopes and scope functions. Kotlin developers use them frequently, and in some cases, they can even be a helpful replacement for extension functions!
When the functions are called in the same order as we would 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 here: https://www.martinfowler.com/bliki/FluentInterface.html ↩︎