Section III: Usability
Computer languages differ not so much in what they make possible, but in what they make easy—Larry Wall, inventor of the Perl language
Extension Functions
Suppose you discover a library that does everything you need … almost. If it only had one or two additional member functions, it would solve your problem perfectly.
But it’s not your code—either you don’t have access to the source code or you don’t control it. You’d have to repeat your modifications every time a new version came out.
Kotlin’s extension functions effectively add member functions to existing classes. The type you extend is called the receiver. To define an extension function, you precede the function name with the receiver type:
fun ReceiverType.extensionFunction() { ... }
This adds two extension functions to the String class:
// ExtensionFunctions/Quoting.kt
package extensionfunctions
import atomictest.eq
fun String.singleQuote() = "'$this'"
fun String.doubleQuote() = "\"$this\""
fun main() {
"Hi".singleQuote() eq "'Hi'"
"Hi".doubleQuote() eq "\"Hi\""
}
You call extension functions as if they were members of the class.
To use extensions from another package, you must import them:
// ExtensionFunctions/Quote.kt
package other
import atomictest.eq
import extensionfunctions.doubleQuote
import extensionfunctions.singleQuote
fun main() {
"Single".singleQuote() eq "'Single'"
"Double".doubleQuote() eq "\"Double\""
}
You can access member functions or other extensions using the this keyword.
this can also be omitted in the same way it can be omitted inside a class, so
you don’t need explicit qualification:
// ExtensionFunctions/StrangeQuote.kt
package extensionfunctions
import atomictest.eq
// Apply two sets of single quotes:
fun String.strangeQuote() =
this.singleQuote().singleQuote() // [1]
fun String.tooManyQuotes() =
doubleQuote().doubleQuote() // [2]
fun main() {
"Hi".strangeQuote() eq "''Hi''"
"Hi".tooManyQuotes() eq "\"\"Hi\"\""
}
-
[1]
thisrefers to theStringreceiver. -
[2] We omit the receiver object (
this) of the firstdoubleQuote()function call.
Creating extensions to your own classes can sometimes produce simpler code:
// ExtensionFunctions/BookExtensions.kt
package extensionfunctions
import atomictest.eq
class Book(val title: String)
fun Book.categorize(category: String) =
"""title: "$title", category: $category"""
fun main() {
Book("Dracula").categorize("Vampire") eq
"""title: "Dracula", category: Vampire"""
}
Inside categorize(), we access the title property without explicit
qualification.
- -
Extension functions can only access public elements of the type
being extended. Thus, extensions can only perform the same actions as regular
functions. You can rewrite Book.categorize(String) as categorize(Book,
String). The only reason for using an extension function is the syntax, but
this syntax sugar is powerful. To the calling code, extensions look the same as
member functions, and IDEs show extensions when listing the functions that you
can call for an object.
Exercises and solutions can be found at www.AtomicKotlin.com.
Named & Default Arguments
You can provide argument names during a function call.
Named arguments improve code readability. This is especially true for long and complex argument lists—named arguments can be clear enough that the reader can understand a function call without looking at the documentation.
In this example, all parameters are Int. Named arguments clarify their
meaning:
// NamedAndDefaultArgs/NamedArguments.kt
package color1
import atomictest.eq
fun color(red: Int, green: Int, blue: Int) =
"($red, $green, $blue)"
fun main() {
color(1, 2, 3) eq "(1, 2, 3)" // [1]
color(
red = 76, // [2]
green = 89,
blue = 0
) eq "(76, 89, 0)"
color(52, 34, blue = 0) eq // [3]
"(52, 34, 0)"
}
- [1] This doesn’t tell you much. You’ll have to look at the documentation to know what the arguments mean.
- [2] The meaning of every argument is clear.
- [3] You aren’t required to name all arguments.
Named arguments allow you to change the order of the colors. Here, we specify
blue first:
// NamedAndDefaultArgs/ArgumentOrder.kt
import color1.color
import atomictest.eq
fun main() {
color(blue = 0, red = 99, green = 52) eq
"(99, 52, 0)"
color(red = 255, 255, 0) eq
"(255, 255, 0)"
}
You can mix named and regular (positional) arguments. If you change argument order, you should use named arguments throughout the call—not only for readability, but the compiler often needs to be told where the arguments are.
Named arguments are even more useful when combined with default arguments, which are default values for arguments, specified in the function definition:
// NamedAndDefaultArgs/Color2.kt
package color2
import atomictest.eq
fun color(
red: Int = 0,
green: Int = 0,
blue: Int = 0,
) = "($red, $green, $blue)"
fun main() {
color(139) eq "(139, 0, 0)"
color(blue = 139) eq "(0, 0, 139)"
color(255, 165) eq "(255, 165, 0)"
color(red = 128, blue = 128) eq
"(128, 0, 128)"
}
Any argument you don’t provide gets its default value, so you only need to provide arguments that differ from the defaults. If you have a long argument list, this simplifies the resulting code, making it easier to write and—more importantly—to read.
This example also uses a trailing comma in the definition for color(). The
trailing comma is the extra comma after the last parameter (blue). This is
useful when your parameters or values are written on multiple lines. With a
trailing comma, you can add new items and change their order without adding or
removing commas.
Named and default arguments (as well as trailing commas) also work for constructors:
// NamedAndDefaultArgs/Color3.kt
package color3
import atomictest.eq
class Color(
val red: Int = 0,
val green: Int = 0,
val blue: Int = 0,
) {
override fun toString() =
"($red, $green, $blue)"
}
fun main() {
Color(red = 77).toString() eq "(77, 0, 0)"
}
joinToString() is a standard library function that uses default arguments. It
combines the contents of an iterable (a list, set or range) into a String.
You can specify a separator, a prefix element and a postfix element:
// NamedAndDefaultArgs/CreateString.kt
import atomictest.eq
fun main() {
val list = listOf(1, 2, 3,)
list.toString() eq "[1, 2, 3]"
list.joinToString() eq "1, 2, 3"
list.joinToString(prefix = "(",
postfix = ")") eq "(1, 2, 3)"
list.joinToString(separator = ":") eq
"1:2:3"
}
The default toString() for a List returns the contents in square brackets,
which might not be what you want. The default values for joinToString()s
parameters are a comma for separator and empty Strings for prefix and
postfix. In the above example, we use named and default arguments to specify
only the arguments we want to change.
The initializer for list includes a trailing comma. Normally you’ll only use
a trailing comma when each element is on its own line.
If you use an object as a default argument, a new instance of that object is created for each invocation:
If you pass an object instance as a default argument (da within g() in the
following example), that same instance is used for each call to g(). If you
pass the syntax for a constructor call (DefaultArg() within h()), that
constructor is called every time you call h():
// NamedAndDefaultArgs/Evaluation.kt
package namedanddefault
class DefaultArg
val da = DefaultArg()
fun g(d: DefaultArg = da) = println(d)
fun h(d: DefaultArg = DefaultArg()) =
println(d)
fun main() {
g()
g()
h()
h()
}
/* Sample output:
namedanddefault.DefaultArg@7440e464
namedanddefault.DefaultArg@7440e464
namedanddefault.DefaultArg@49476842
namedanddefault.DefaultArg@78308db1
*/
The output of the two g() calls shows identical object addresses. For the two
calls to h(), the addresses of the DefaultArg objects are different, showing
that there are two distinct objects.
Specify argument names when they improve readability. Compare the following two
calls to joinToString():
// NamedAndDefaultArgs/CreateString2.kt
import atomictest.eq
fun main() {
val list = listOf(1, 2, 3)
list.joinToString(". ", "", "!") eq
"1. 2. 3!"
list.joinToString(separator = ". ",
postfix = "!") eq "1. 2. 3!"
}
It’s hard to guess whether ". " or "" is a separator unless you memorize
the parameter order, which is impractical.
As another example of default arguments, trimMargin() is a standard library
function that formats multi-line Strings. It uses a margin prefix String to
establish the beginning of each line. trimMargin() trims leading whitespace
characters followed by the margin prefix from every line of the source
String. It removes the first and last lines if they are blank:
// NamedAndDefaultArgs/TrimMargin.kt
import atomictest.eq
fun main() {
val poem = """
|->Last night I saw upon the stair
|->A little man who wasn't there
|->He wasn't there again today
|->Oh, how I wish he'd go away."""
poem.trimMargin() eq
"""->Last night I saw upon the stair
->A little man who wasn't there
->He wasn't there again today
->Oh, how I wish he'd go away."""
poem.trimMargin(marginPrefix = "|->") eq
"""Last night I saw upon the stair
A little man who wasn't there
He wasn't there again today
Oh, how I wish he'd go away."""
}
The | (“pipe”) is the default argument for the margin prefix, and you can
replace it with a String of your choosing.
Exercises and solutions can be found at www.AtomicKotlin.com.
Overloading
Languages without support for default arguments often use overloading to imitate that feature.
The term overload refers to the name of a function: You use the same name
(“overload” that name) for different functions as long as the parameter lists
differ. Here, we overload the member function f():
// Overloading/Overloading.kt
package overloading
import atomictest.eq
class Overloading {
fun f() = 0
fun f(n: Int) = n + 2
}
fun main() {
val o = Overloading()
o.f() eq 0
o.f(11) eq 13
}
In Overloading, you see two functions with the same name, f(). The
function’s signature consists of the name, parameter list and return type.
Kotlin distinguishes one function from another by comparing signatures. When
overloading functions, the parameter lists must be unique—you cannot overload
on return types.
The calls show that they are indeed different functions. A function signature also includes information about the enclosing class (or the receiver type, if it’s an extension function).
If a class already has a member function with the same signature as an extension function, Kotlin prefers the member function. However, you can overload the member function with an extension function:
// Overloading/MemberVsExtension.kt
package overloading
import atomictest.eq
class My {
fun foo() = 0
}
fun My.foo() = 1 // [1]
fun My.foo(i: Int) = i + 2 // [2]
fun main() {
My().foo() eq 0
My().foo(1) eq 3
}
- [1] It’s senseless to declare an extension that duplicates a member, because it can never be called.
- [2] You can overload a member function using an extension function by providing a different parameter list.
Don’t use overloading to imitate default arguments. That is, don’t do this:
// Overloading/WithoutDefaultArguments.kt
package withoutdefaultarguments
import atomictest.eq
fun f(n: Int) = n + 373
fun f() = f(0)
fun main() {
f() eq 373
}
The function without parameters just calls the first function. The two functions can be replaced with a single function by using a default argument:
// Overloading/WithDefaultArguments.kt
package withdefaultarguments
import atomictest.eq
fun f(n: Int = 0) = n + 373
fun main() {
f() eq 373
}
In both examples you can call the function either without an argument or by
passing an integer value. Prefer the form in WithDefaultArguments.kt.
When using overloaded functions together with default arguments, calling the
overloaded function searches for the “closest” match. In the following example,
the foo() call in main() does not call the first version of the function
using its default argument of 99, but instead calls the second version, the one
without parameters:
// Overloading/OverloadedVsDefaultArg.kt
package overloadingvsdefaultargs
import atomictest.*
fun foo(n: Int = 99) = trace("foo-1-$n")
fun foo() {
trace("foo-2")
foo(14)
}
fun main() {
foo()
trace eq """
foo-2
foo-1-14
"""
}
You can never utilize the default argument of 99, because foo() always
calls the second version of f().
Why is overloading useful? It allows you to express “variations on a theme” more clearly than if you were forced to use different function names. Suppose you want addition functions:
// Overloading/OverloadingAdd.kt
package overloading
import atomictest.eq
fun addInt(i: Int, j: Int) = i + j
fun addDouble(i: Double, j: Double) = i + j
fun add(i: Int, j: Int) = i + j
fun add(i: Double, j: Double) = i + j
fun main() {
addInt(5, 6) eq add(5, 6)
addDouble(56.23, 44.77) eq
add(56.23, 44.77)
}
addInt() takes two Ints and returns an Int, while addDouble() takes
two Doubles and returns a Double. Without overloading, you can’t just name
the operation add(), so programmers typically conflate what with how to
produce unique names (you can also create unique names using random characters
but the typical pattern is to use meaningful information like parameter types).
In contrast, the overloaded add() is much clearer.
- -
The lack of overloading in a language is not a terrible hardship, but the
feature provides valuable simplification, producing more readable code. With
overloading, you just say what, which raises the level of abstraction and
puts less mental load on the reader. If you want to know how, look at the
parameters. Notice also that overloading reduces redundancy: If we must say
addInt() and addDouble(), then we essentially repeat the parameter
information in the function name.
Exercises and solutions can be found at www.AtomicKotlin.com.