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
.
Use the apply
function.
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
.
apply
functioninline
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.
@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.
Use the also
function to perform the action.
The function also
is an extension function in the standard library, whose implementation is shown in Example 7-3.
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.
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.
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
(
)
)
}
.
run
{
assertThat
(
latitude
,
`is`
(
closeTo
(
4
2.36
,
0.01
)
)
)
assertThat
(
longitude
,
`is`
(
closeTo
(
-
7
1.06
,
0.01
)
)
)
}
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.
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.
Recipe 7.1 discusses the apply
function.
Use the let
scope function with a safe call, combined with the Elvis operator.
The let
function is an extension function on any generic type T
, whose implementation in the standard library is given by Example 7-6.
let
in the standard librarypublic
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.
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.
fun
processNullableString
(
str
:
String
?
)
=
str
?.
let
{
when
{
it
.
isEmpty
(
)
-
>
"Empty"
it
.
isBlank
(
)
-
>
"Blank"
else
-
>
it
.
capitalize
(
)
}
}
?:
"Null"
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.
The also
block is discussed in Recipe 7.2. Using let
as a replacement for temporary variables is shown in Recipe 7.4.
Chain a let
call to the calculation and process the result in the supplied lambda or function reference.
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.
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
)
After refactoring to use a let
block, the code looks like Example 7-10.
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.
let
blockval
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
.
Gson
(
)
.
fromJson
(
URL
(
"http://api.open-notify.org/astros.json"
)
.
readText
(
)
,
AstroResult
::
class
.
java
)
.
people
.
map
{
it
.
name
}
.
let
(
::
println
)
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 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.
3.146.221.144