Master Kotlin standard functions: run, with, let, also and apply

Keywords: Fragment Programming Lambda

Links to the original text

Some of Kotlin's Standard function Very similar, we are not sure which function to use. Here I will introduce a simple way to clearly distinguish their differences and how to choose to use them.

Range function

I focus on run, with, T.run, T.let, T.also and T.apply functions. I call them scope functions because I think their main function is to provide an internal scope for calling functions.

run function is the simplest range method

fun test() {
    var mood = "I am sad"

    run {
        val mood = "I am happy"
        println(mood) // I am happy
    }
    println(mood)  // I am sad
}

With this function, inside the test function, you can have a separate range, mood is completely enclosed in run before it is redefined as I am happy and printed.

The scope function itself does not seem to be very useful. But another good thing about scoping is that it returns the last object in the scope.

Therefore, the following code will be pure, and we can apply the show() method to two view s instead of calling it twice as follows.

run {
      if (firstTimeView) introView else normalView
    }.show()

Three Properties of Range Function

To make scoping functions more interesting, let me categorize their behavior with three attributes, and use these attributes to distinguish them.

1. Normal vs. Extension Function

If we look at the definition, with and T.run are actually very similar. The following example implements the same functionality.

with(webview.settings) {
    javaScriptEnabled = true
    databaseEnabled = true
}
// Be similar
webview.settings.run {
    javaScriptEnabled = true
    databaseEnabled = true
}

However, their difference is that with is a normal function, while T.run is an extended function.

So the question is, what are the advantages of each?

Imagine if webview.settings might be empty, it would look like the following.

with(webview.settings) {
      this?.javaScriptEnabled = true
      this?.databaseEnabled = true
   }
}

webview.settings?.run {
    javaScriptEnabled = true
    databaseEnabled = true
}

In this case, it's obvious that T.run extensions are better because we can empty them before using them.

2.This vs. it parameter

If we look at the definition, the functions T.run and T.let are almost the same except that they accept parameters differently. The logic of the following two functions is the same.

stringVariable?.run {
      println("The length of this String is $length")
}

stringVariable?.let {
      println("The length of this String is ${it.length}")
}

If you check the signature of the T.run function, you will notice that T.run only calls block: T. () as an extension function. Therefore, in all ranges, T can be called this. In programming, this can be omitted most of the time. So in our example above, we can use $length in the println declaration instead of ${this.length}. I call this the pass this parameter.

However, for T.let function signatures, you will notice that T.let passes itself in as a parameter, namely block: (T). So, it's like passing a lambda parameter. It can be used as a reference within scope. So I call it passing it parameters.

From the above point of view, it seems that T.run is superior, because T.let is more implicit, but this T.let function has some subtle advantages as follows:

  • T.let provides a clearer distinction with a given variable function/member than with an external class function/member.
  • When this cannot be omitted, for example, it is shorter and clearer than this when it is passed as a function parameter.
  • Better variable naming is allowed in T.let, and you can convert it to other names.
stringVariable?.let {
      nonNullString ->
      println("The non null string is $nonNullString")
}

3. Return the current type vs. other types

Now, let's look at T.let and T.also, if we look at their internal function scopes, they're the same.

stringVariable?.let {
      println("The length of this String is ${it.length}")
}
stringVariable?.also {
      println("The length of this String is ${it.length}")
}

However, their subtle difference is their return value. T.let returns different types of values, while T.also returns T itself, that is, this.

Both are useful for linking functions. You can evolve operations through T.let, and you can perform operations on the same variable this through T.also.

A simple example is as follows

val original = "abc"
// Change the value and pass it to the next chain
original.let {
    println("The original String is $it") // "abc"
    it.reversed() // Change the parameters and pass them to the next chain
}.let {
    println("The reverse String is $it") // "cba"
    it.length   // Change the type
}.let {
    println("The length of the String is $it") // 3
}
// error
// Send the same value in the chain (the printed answer is wrong)
original.also {
    println("The original String is $it") // "abc"
    it.reversed() // Even if we change it, it's useless.
}.also {
    println("The reverse String is ${it}") // "abc"
    it.length  // Even if we change it, it's useless.
}.also {
    println("The length of the String is ${it}") // "abc"
}

// also can achieve the same goal by modifying the original string
// Send the same value in the chain
original.also {
    println("The original String is $it") // "abc"
}.also {
    println("The reverse String is ${it.reversed()}") // "cba"
}.also {
    println("The length of the String is ${it.length}") // 3
}

T.also seems pointless above, because we can easily combine them into a functional block. But think carefully, it also has some advantages:

  1. It can provide a very clear separation process on the same object, that is, making smaller functional parts.
  2. Before using it, it can achieve very powerful self-manipulation and chain builder operation (builder mode).

When the two are combined, that is, a self-evolution, a self-preservation, can become very powerful, such as the following

// Normal method
fun makeDir(path: String): File  {
    val result = File(path)
    result.mkdirs()
    return result
}
// Improvement method
fun makeDir(path: String) = path.let{ File(it) }.also{ it.mkdirs() }

All attributes

By explaining these three attributes, we should be able to understand the behavior of these functions. Let's look at the T.apply function again, because it's not mentioned above. These three attributes are defined in T.apply as follows.

  1. This is an extension function.
  2. Pass this as a parameter.
  3. It returns this (that is, it itself)

So, as you can imagine, it can be used as follows

// Normal method
fun createInstance(args: Bundle) : MyFragment {
    val fragment = MyFragment()
    fragment.arguments = args
    return fragment
}
// Improvement method
fun createInstance(args: Bundle) 
              = MyFragment().apply { arguments = args }

Or we can create chain calls.

// Normal method
fun createIntent(intentData: String, intentAction: String): Intent {
    val intent = Intent()
    intent.action = intentAction
    intent.data=Uri.parse(intentData)
    return intent
}
//  Improving implementation
fun createIntent(intentData: String, intentAction: String) =
        Intent().apply { action = intentAction }
                .apply { data = Uri.parse(intentData) }

Function selection

Therefore, obviously, with these three attributes, we can now classify the above functions accordingly. On this basis, we can form a decision tree below to help us decide which function to use.

img

It is hoped that the decision tree above can clearly illustrate the difference between functions and simplify your decision-making so that you can properly grasp the use of these functions.

Posted by nicholasstephan on Thu, 27 Dec 2018 14:33:06 -0800