Chapter 7. Scope Functions

The Kotlin standard library contains several functions whose purpose is to execute a block of code in the context of an object. Specifically, this chapter discusses the scope functions let, run, apply, and also.

7.1 Initializing Objects After Construction with apply

Problem

You need to initialize an object before using it, beyond what you can do by supplying constructor arguments.

Solution

Use the apply function.

Discussion

Kotlin has several scoping functions that you can apply to objects. The apply function is an extension function that sends this as an argument and returns it as well. Example 7-1 shows the definition of apply.

Example 7-1. Definition of the apply function
inline fun <T> T.apply(block: T.() -> Unit): T

The apply function is thus an extension function on any generic type T, which calls the specified block with this as its receiver and returns this when it completes.

As a practical example, consider the problem of saving an object to a relational database by using the Spring framework. Spring provides a class called SimpleJdbcInsert, based on JdbcTemplate, which removes the boilerplate from normal JDBC code in Java.

Say we have an entity called Officer that maps to a database table called OFFICERS. Writing the SQL INSERT statement for such a class is straightforward, except for one complication: if the primary key is generated by the database during the save, then the supplied object needs to be updated with the new key. For this purpose, the SimpleJdbcInsert class has a convenient method called executeAndReturnKey, which takes a map of column names to values and returns the generated value.

Using the apply function, the save function can receive an instance to be saved and update it with the new key all in one statement, as in Example 7-2.

Example 7-2. Inserting a domain object and updating the generated key
@Repository
class JdbcOfficerDAO(private val jdbcTemplate: JdbcTemplate) {

    private val insertOfficer = SimpleJdbcInsert(jdbcTemplate)
            .withTableName("OFFICERS")
            .usingGeneratedKeyColumns("id")

    fun save(officer: Officer) =
        officer.apply {
            id = insertOfficer.executeAndReturnKey(
                    mapOf("rank" to rank,
                          "first_name" to first,
                          "last_name" to last))
            }

// ...

}

The Officer instance is passed into the apply block as this, so it can be used to access the properties rank, first, and last. The id property of the officer is updated inside the apply block, and the officer instance is returned. Additional initialization could be chained to this block if necessary or desired.

The apply block is useful if the result needs to be the context object (the officer in this example). It is most commonly used to do additional configuration of objects that have already been instantiated.

7.2 Using also for Side Effects

Problem

You want to print a message or other side effect without interrupting the flow of your code.

Solution

Use the also function to perform the action.

Discussion

The function also is an extension function in the standard library, whose implementation is shown in Example 7-3.

Example 7-3. The extension function also
public inline fun <T> T.also(
    block: (T) -> Unit
): T

As the code shows, also is added to any generic type T, which it returns after executing the block argument. It is most commonly used to chain a function call onto an object, as in Example 7-4.

Example 7-4. Printing and logging with also
val book = createBook()
    .also { println(it) }
    .also { Logger.getAnonymousLogger().info(it.toString()) }

Inside the block, the object is referenced as it.

Because also returns the context object, it’s easy to chain additional calls together, as shown here, where the book was first printed to the console and then logged somewhere.

While it’s useful to see that you can chain multiple also calls together, the function is more typically added as part of a series of business logic calls. For example, consider a test of a geocoder service, given by Example 7-5.

Example 7-5. Testing a geocoder service
class Site(val name: String,
           val latitude: Double,
           val longitude: Double)

// ... inside test class ...

@Test
fun `lat,lng of Boston, MA`() = service.getLatLng("Boston", "MA")
    .also { logger.info(it.toString()) }  1
    .run {
        assertThat(latitude, `is`(closeTo(42.36, 0.01)))
        assertThat(longitude, `is`(closeTo(-71.06, 0.01)))
    }
1

Logging as a side effect

This test could be organized in many ways, but using also in this way implies that the point of this code is to run the tests, but also to print the site. Note that using the scope functions converts the entire test into a single expression, allowing for the shorter syntax.

Caution

The also call has to come before the run call in the test, because run returns the value of the lambda rather than the context object.

Incidentally, although you could replace the run call with apply, JUnit tests are supposed to return Unit. The run call in Example 7-5 does that (because the assertions don’t return anything), while apply would return the context object.

See Also

Recipe 7.1 discusses the apply function.

7.3 Using the let Function and Elvis

Problem

You want to execute a block of code only on a non-null reference, but return a default otherwise.

Solution

Use the let scope function with a safe call, combined with the Elvis operator.

Discussion

The let function is an extension function on any generic type T, whose implementation in the standard library is given by Example 7-6.

Example 7-6. Implementation of let in the standard library
public inline fun <T, R> T.let(
    block: (T) -> R
): R

The key fact to remember about let is that it returns the result of the block, rather than the context object. It therefore acts like a transformation of the context object, sort of like a map for objects. Say you want to take a string and capitalize it, but require special handling for empty or blank strings, as in Example 7-7.

Example 7-7. Capitalizing a string with special cases
fun processString(str: String) =
    str.let {
        when {
            it.isEmpty() -> "Empty"
            it.isBlank() -> "Blank"
            else -> it.capitalize()
        }
    }

Normally, you would just call the capitalize function, but on empty or blank strings this wouldn’t give back anything useful. The let function allows you to wrap the when conditional inside a block that handles all the required cases, and returns the “transformed” string.

This really becomes interesting, however, when the argument is nullable, as in Example 7-8.

Example 7-8. Same process, but with a nullable string
fun processNullableString(str: String?) =
    str?.let {          1
        when {
            it.isEmpty() -> "Empty"
            it.isBlank() -> "Blank"
            else -> it.capitalize()
        }
    } ?: "Null"         2
1

Safe call with a let

2

Elvis operator to handle the null case

The return type on both functions is String, which is inferred from the execution.

In this case, the combination of the safe call operator ?., the let function, and the Elvis operator ?: combine to handle all cases easily. This is a common idiom in Kotlin, as it lets (sorry) you handle both the null and non-null cases easily.

Many Java APIs (like Spring’s RestTemplate or WebClient) return nulls when there is no result, and the combination of a safe call, a let block, and an Elvis operator is an effective means of handling them.

See Also

The also block is discussed in Recipe 7.2. Using let as a replacement for temporary variables is shown in Recipe 7.4.

7.4 Using let with a Temporary Variable

Problem

You want to process the result of a calculation without needing to assign the result to a temporary variable.

Solution

Chain a let call to the calculation and process the result in the supplied lambda or function reference.

Discussion

The documentation pages for scope functions at the Kotlin website show an interesting use case for the let function. Their example (repeated in Example 7-9) creates a mutable list of strings, maps them to their lengths, and filters the result.

Example 7-9. let example from online docs, before refactoring
// Before
val numbers = mutableListOf("one", "two", "three", "four", "five")
val resultList = numbers.map { it.length }.filter { it > 3 }
println(resultList)  1
1

Assigns calculation to temp variable for printing

After refactoring to use a let block, the code looks like Example 7-10.

Example 7-10. After refactoring to use let
// After
val numbers = mutableListOf("one", "two", "three", "four", "five")
numbers.map { it.length }.filter { it > 3 }.let {
    println(it)
    // and more function calls if needed
}

The idea is that rather than assign the result to a temporary variable, the chained let call uses the result as its context variable, so it can be printed (or more) in the provided block. If all that is required is to print the result, this can even be reduced further, to the form in Example 7-11.

Example 7-11. Using a function reference in the let block
val numbers = mutableListOf("one", "two", "three", "four", "five")
numbers.map { it.length }.filter { it > 3 }.let(::println)

As a slightly more interesting example, consider a class that accesses a remote service at Open Notify that returns the number of astronauts in space, as described in Recipe 11.6 later. The service returns JavaScript Object Notation (JSON) data and transforms the result into instances of classes that you’ll see again in Example 11-17:

data class AstroResult(
    val message: String,
    val number: Number,
    val people: List<Assignment>
)

data class Assignment(
    val craft: String,
    val name: String
)

Example 7-12 uses the extension method URL.readText and Google’s Gson library to convert the received JSON into an instance of AstroResult.

Example 7-12. Printing the names of astronauts currently in space
Gson().fromJson(
    URL("http://api.open-notify.org/astros.json").readText(),
        AstroResult::class.java
    ).people.map { it.name }.let(::println)  1
1

Uses let (or also) to print the List<String>

In this case, the basic code in the Gson().fromJson call converts the JSON data into an instance of AstroResult. The map function then transforms the Assignment instances into a list of strings representing the astronaut names.

As of August 2019, the output from this program is (all on one line):

[Alexey Ovchinin, Nick Hague, Christina Koch,
    Alexander Skvortsov, Luca Parmitano, Andrew Morgan]

In this case, the let in Example 7-12 could be replaced with an also. The difference is that let returns the result of the block (Unit in the case of println) while also would return the context object (the List<String>). Neither is used after the print, so the difference in this case doesn’t matter. It might be more idiomatic to use also, since that is typically used for side effects like printing. Either way works, though.

See Also

See Recipe 7.3 for how to use let with a safe call and Elvis operator in the case of nullable values. The also function is discussed in Recipe 7.2.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.146.221.144