Declaring Extension Functions

Declaring an extension function looks and works a lot like declaring any other function. The difference is that the type the extension function is being declared upon is included as a prefix to the function name, separated by ..

private val EMAIL_REGEX = Regex("^[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+[.]+[a-zA-Z0-9-.]+(\s)*")

fun String.isValidEmail() = EMAIL_REGEX.matches(this)

fun main() {
  println("martians-so-do-not-exist@commonsware.com".isValidEmail())
  println("this is not an email address".isValidEmail())
}

Here, as with the JavaScript and Ruby examples, we are adding an isValidEmail() function to Kotlin’s existing String type. We can then call isValidEmail() on candidate email addresses, whether those come from users or are hard-coded test values, such as "martians-so-do-not-exist@commonsware.com".

The Syntax

The extension function behaves a lot like a regular function that you might have on the class. In particular, this is how you refer to the instance of the type. So, in String.isValidEmail(), this is the String on which you called isValidEmail().

The type on which you declare the extension function can be any valid Kotlin type. This includes nullable types, though the function implementation may wind up being more complex as a result:

private val EMAIL_REGEX = Regex("^[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+[.]+[a-zA-Z0-9-.]+(\s)*")

fun String?.isValidEmail() = this?.let { EMAIL_REGEX.matches(this) } ?: false

fun main() {
  println("martians-so-do-not-exist@commonsware.com".isValidEmail())
  println("this is not an email address".isValidEmail())
  println(null.isValidEmail())
}

Here, we declare isValidEmail() on String? instead of String, allowing us to call isValidEmail() on null as well as on an actual String. However, matches() on a Regex does not accept a null parameter, so we use let() and the Elvis operator to supply false as a default value for the null case.

The Location and Visibility

You can declare an extension function anywhere that you can declare a regular function. So, in the above example, the extension function is a top-level function.

You can make extension functions be public (the default) or private, as needed.

This leads to three common patterns:

Public Top-Level

A public top-level extension function adds that function to the designated type for anything using your code. This is good for utility functions that will be used widely in the project.

Private Top-Level

A private top-level extension function is usable only within the source file that contains that function. This is good for utility functions whose implementation is fairly tightly tied to the code in the source file and would not be useful elsewhere. The private nature means that you will not collide with other private implementations in other source files.

Class Members

You can also have class members that are extension functions, limiting their access to instances of that class:

private val EMAIL_REGEX = Regex("^[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+[.]+[a-zA-Z0-9-.]+(\s)*")

data class User(val userId: String)

class UserRegistration {
  private fun String.isValidEmail() = EMAIL_REGEX.matches(this)

  fun isValidUser(user: User) = user.userId.isValidEmail()
}

fun main() {
  val registrar = UserRegistration()

  println(registrar.isValidUser(User(userId = "martians-so-do-not-exist@commonsware.com")))
  println(registrar.isValidUser(User(userId = "this is not an email address")))
}

Here, isValidEmail() is an extension function on String, but that extension function is only accessible to instances of UserRegistration.

This could just as easily be written as:

class UserRegistration {
  private fun isValidEmail(value: String) = emailRegex.matches(value)

  fun isValidUser(user: User) = isValidEmail(user.userId)
}

Certainly, this second approach is more conventional. In the end, it comes down to object design. In this case, should a String know if it is a valid email address? Or is that really a responsibility of something else, such as UserRegistration?


Prev Table of Contents Next

This book is licensed under the Creative Commons Attribution-ShareAlike 4.0 International license.