Section III: Usability

Computer languages differ not so much in what they make possible, but in what they make easyLarry 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] this refers to the String receiver.
  • [2] We omit the receiver object (this) of the first doubleQuote() 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.