In depth explanation of higher order functions in Kotlin

  • 2021-10-16 05:03:40
  • OfStack

Preface

In Kotlin, a higher-order function refers to taking one function as an argument or return value of another function. If f (x) and g (x) are used to represent two functions, then the higher order function can be expressed as f (g (x). Kotlin provides developers with rich high-order functions, such as let, with, apply in Standard. kt, forEach in _ Collectioins. kt, etc. In order to be able to use these higher order functions freely, we need to understand how to use these higher order functions.

Function type

Before introducing the common use of higher-order functions, it is necessary to understand the function types, which is very helpful for us to understand higher-order functions. Kotlin uses something like (Int)- > String's 1 series of function types, which have special notations corresponding to function signatures, i.e. their parameters and return values:

All function types have a parenthesized list of parameter types and a return type: (A, B)- > C represents a function type that accepts two parameters of type A and B and returns a value of type C. The list of parameter types can be empty, such as ()- > A, the return value is null, such as (A, B)- > Unit; Function types can have 1 additional recipient type, which is specified before the dot in the notation, such as type A. (B)- > C means you can call a function on the recipient object of A that takes B type as an argument and returns a value of C type. There is also a special type of function, suspended function, which has an suspend modifier in its notation, such as suspend ()- > Unit or suspend A. (B)- > C.

Commonly used higher order functions

Kotlin provides many high-order functions. Here, according to the location of these high-order functions in the file, they are introduced respectively. First, look at the commonly used high-order functions under 1. These high-order functions are in Standard. kt file.

1.TODO

First look at the source code of TODO:


/**
 * Always throws [NotImplementedError] stating that operation is not implemented.
 */

@kotlin.internal.InlineOnly
public inline fun TODO(): Nothing = throw NotImplementedError()

/**
 * Always throws [NotImplementedError] stating that operation is not implemented.
 *
 * @param reason a string explaining why the implementation is missing.
 */
@kotlin.internal.InlineOnly
public inline fun TODO(reason: String): Nothing = throw NotImplementedError("An operation is not implemented: $reason")

The TODO function has two overloaded functions, each of which throws an NotImplementedError exception. In Java, TODO identifiers are sometimes added to unrealized logic in order to keep the continuity of business logic. These identifiers are not processed and will not lead to program anomalies. However, when TODO is used in Kotlin, these identifiers need to be processed, otherwise, when the code logic runs to these identifiers, the program will crash.

2.run

First, the source code of run function is given:


/**
 * Calls the specified function [block] and returns its result.
 */
@kotlin.internal.InlineOnly
public inline fun <R> run(block: () -> R): R {
 contract {
 callsInPlace(block, InvocationKind.EXACTLY_ONCE)
 }
 return block()
}

/**
 * Calls the specified function [block] with `this` value as its receiver and returns its result.
 */
@kotlin.internal.InlineOnly
public inline fun <T, R> T.run(block: T.() -> R): R {
 contract {
 callsInPlace(block, InvocationKind.EXACTLY_ONCE)
 }
 return block()
}

Both run functions receive an lambda expression, execute the incoming lambda expression, and return the execution result of the lambda expression. The difference is that T. run () is an extension function of the generic T, so the this keyword can be used in the passed-in lambda expression to access the member variables and member methods in this generic T.

For example, for an EditText control, when you make one set:


//email  Yes 1 A EditText Control 
email.run { 
  this.setText(" Please enter your email address ")
  setTextColor(context.getColor(R.color.abc_btn_colored_text_material))
}

3.with

First look at the source code of with function under 1:


/**
 * Calls the specified function [block] with the given [receiver] as its receiver and returns its result.
 */
@kotlin.internal.InlineOnly
public inline fun <T, R> with(receiver: T, block: T.() -> R): R {
 contract {
 callsInPlace(block, InvocationKind.EXACTLY_ONCE)
 }
 return receiver.block()
}

The with function takes two arguments, an receiver of type generic T, and an lambda expression that executes as an extension function of receiver and returns the execution result of the lambda expression.

The with function differs from the T. run function only in writing. For example, the above example can use the with function:


 with(email, {
  setText(" Please enter your email address ")
  setTextColor(context.getColor(R.color.abc_btn_colored_text_material))
 })
 
 // Can enter 1 Step is simplified to 
 with(email) {
  setText(" Please enter your email address ")
  setTextColor(context.getColor(R.color.abc_btn_colored_text_material))
 }

4.apply

Look at the source code of apply function under 1:


/**
 * Calls the specified function [block] with `this` value as its receiver and returns `this` value.
 */
@kotlin.internal.InlineOnly
public inline fun <T> T.apply(block: T.() -> Unit): T {
 contract {
 callsInPlace(block, InvocationKind.EXACTLY_ONCE)
 }
 block()
 return this
}

As an extension function of generic T, the apply function receives an lambda expression. The receiver of the expression is generic T and has no return value. The apply function returns the generic T object itself. You can see that the T. run () function also receives the lambda expression, but the return value is the execution result of the lambda expression, which is the biggest difference from the apply function.

Again, you can use the apply function:


 email.apply { 
  setText(" Please enter your email address ")
 }.apply {
  setTextColor(context.getColor(R.color.abc_btn_colored_text_material))
 }.apply { 
  setOnClickListener { 
  TODO()
  }
 }

5.also

Look at the source code of also function under 1:


/**
 * Calls the specified function [block] with `this` value as its argument and returns `this` value.
 */
@kotlin.internal.InlineOnly
@SinceKotlin("1.1")
public inline fun <T> T.also(block: (T) -> Unit): T {
 contract {
  callsInPlace(block, InvocationKind.EXACTLY_ONCE)
 }
 block(this)
 return this
}

Similar to the apply function, it is also an extension function of the generic T, and receives an lambda expression, and the lambda expression has no return value. The also function also returns the generic T object itself. The difference is that the lambda expression received by the also function needs to receive one parameter T, so it can be used inside the lambda expression, while only this can be used in apply.

The difference between this and it is summarized as follows:

If the generic T, as an argument to the lambda expression, looks like: (T)- > Unit, where lambda denotes internal use of it; If the generic T, as the recipient of the lambda expression, looks like T. ()- > Unit, where this is used inside the lambda expression; Both this and it represent T objects, except that it can be replaced by other names.

Again, if you use the also function:


 email.also { 
   it.setText(" Please enter your email address ")
  }.also { 
   // You can use other names 
   editView -> editView.setTextColor(applicationContext.getColor(R.color.abc_btn_colored_text_material))
  }.also { 
   it.setOnClickListener { 
    //TODO
   }
  }

6.let

Look at the source code of let function under 1:


/**
 * Calls the specified function [block] with `this` value as its argument and returns its result.
 */
@kotlin.internal.InlineOnly
public inline fun <T, R> T.let(block: (T) -> R): R {
 contract {
  callsInPlace(block, InvocationKind.EXACTLY_ONCE)
 }
 return block(this)
}

As an extension function of generic T, let function receives one lambda expression, and lambda expression needs to receive one parameter T, and there is a return value. The return value of the lambda expression is the return value of the let function. Because the lambda expression accepts the parameter T, you can also use it inside it.

The most widely used scenario of let is to judge nullness. If EditText in the above example is a custom nullable View, it is very convenient to use let:


/**
 * Calls the specified function [block] and returns its result.
 */
@kotlin.internal.InlineOnly
public inline fun <R> run(block: () -> R): R {
 contract {
 callsInPlace(block, InvocationKind.EXACTLY_ONCE)
 }
 return block()
}

/**
 * Calls the specified function [block] with `this` value as its receiver and returns its result.
 */
@kotlin.internal.InlineOnly
public inline fun <T, R> T.run(block: T.() -> R): R {
 contract {
 callsInPlace(block, InvocationKind.EXACTLY_ONCE)
 }
 return block()
}
0

7.takeIf

Look at the source code of takeIf function under 1:


/**
 * Calls the specified function [block] and returns its result.
 */
@kotlin.internal.InlineOnly
public inline fun <R> run(block: () -> R): R {
 contract {
 callsInPlace(block, InvocationKind.EXACTLY_ONCE)
 }
 return block()
}

/**
 * Calls the specified function [block] with `this` value as its receiver and returns its result.
 */
@kotlin.internal.InlineOnly
public inline fun <T, R> T.run(block: T.() -> R): R {
 contract {
 callsInPlace(block, InvocationKind.EXACTLY_ONCE)
 }
 return block()
}
1

The takeIf function, as an extension function of the generic T, accepts an lambda expression, and the lambda expression receives a parameter T and returns the Boolean type. The takeIf function determines the return value of the function according to the return value of the received lambda expression. If the lambda expression returns true, the function returns T object itself, and if the lambda expression returns false, the function returns null.

Again, assuming that the user did not enter an email address, he was prompted for information:


/**
 * Calls the specified function [block] and returns its result.
 */
@kotlin.internal.InlineOnly
public inline fun <R> run(block: () -> R): R {
 contract {
 callsInPlace(block, InvocationKind.EXACTLY_ONCE)
 }
 return block()
}

/**
 * Calls the specified function [block] with `this` value as its receiver and returns its result.
 */
@kotlin.internal.InlineOnly
public inline fun <T, R> T.run(block: T.() -> R): R {
 contract {
 callsInPlace(block, InvocationKind.EXACTLY_ONCE)
 }
 return block()
}
2

8.takeUnless

The source code of takeUnless function is given:


/**
 * Returns `this` value if it _does not_ satisfy the given [predicate] or `null`, if it does.
 */
@kotlin.internal.InlineOnly
@SinceKotlin("1.1")
public inline fun <T> T.takeUnless(predicate: (T) -> Boolean): T? {
 contract {
  callsInPlace(predicate, InvocationKind.EXACTLY_ONCE)
 }
 return if (!predicate(this)) this else null
}

The takeUnless function is similar to the takeIf function, except that the logic is opposite. The takeUnless function determines the return value of the function according to the return value of the lambda expression. If the lambda expression returns true, the function returns null, and if the lambda expression returns false, the function returns T object itself.

Again, if you implement it with takeUnless, you need to adjust the logic under 1:


/**
 * Calls the specified function [block] and returns its result.
 */
@kotlin.internal.InlineOnly
public inline fun <R> run(block: () -> R): R {
 contract {
 callsInPlace(block, InvocationKind.EXACTLY_ONCE)
 }
 return block()
}

/**
 * Calls the specified function [block] with `this` value as its receiver and returns its result.
 */
@kotlin.internal.InlineOnly
public inline fun <T, R> T.run(block: T.() -> R): R {
 contract {
 callsInPlace(block, InvocationKind.EXACTLY_ONCE)
 }
 return block()
}
4

9.repeat

The source code of repeat function is given:


/**
 * Calls the specified function [block] and returns its result.
 */
@kotlin.internal.InlineOnly
public inline fun <R> run(block: () -> R): R {
 contract {
 callsInPlace(block, InvocationKind.EXACTLY_ONCE)
 }
 return block()
}

/**
 * Calls the specified function [block] with `this` value as its receiver and returns its result.
 */
@kotlin.internal.InlineOnly
public inline fun <T, R> T.run(block: T.() -> R): R {
 contract {
 callsInPlace(block, InvocationKind.EXACTLY_ONCE)
 }
 return block()
}
5

The repeat function receives two parameters, one Int type parameter times denotes the number of repetitions, and one lambda expression and lambda expression receive one Int type parameter with no return value. The repeat function executes the times expression we passed in lambda several times.


/**
 * Calls the specified function [block] and returns its result.
 */
@kotlin.internal.InlineOnly
public inline fun <R> run(block: () -> R): R {
 contract {
 callsInPlace(block, InvocationKind.EXACTLY_ONCE)
 }
 return block()
}

/**
 * Calls the specified function [block] with `this` value as its receiver and returns its result.
 */
@kotlin.internal.InlineOnly
public inline fun <T, R> T.run(block: T.() -> R): R {
 contract {
 callsInPlace(block, InvocationKind.EXACTLY_ONCE)
 }
 return block()
}
6

Because the lambda expression received by repeat function needs an Int parameter, Int is used inside the expression. In fact, it is the index of for loop, starting from 0.

Summarize

Finally, these high-order functions are summarized. Compared with TODO in Java, TODO needs to realize business logic, which cannot be ignored, otherwise there will be anomalies and lead to collapse. takeIf and takeUnless both decide whether the final return value of the function is the object itself or null according to the return value of the received lambda expression. The difference is takeIf. If lambda expression returns true, return the object itself, otherwise return null; The logic of takeUnless is the opposite of that of takeIf. If the lambda expression returns true, it returns null, otherwise it returns the object itself. repeat function, see name know meaning, will receive lambda expression repeated execution specified times.

The difference between run, with, apply, also and let is not obvious. Sometimes, the logic realized by one function can also be realized by another function. Which one to use depends on personal habits. It should be noted that:

For high-order functions as extension functions, it is necessary to judge whether the received objects are empty before use, such as T. run, apply, also and let. For functions that return the object itself, such as apply, also can form chain calls; For functions that can use it inside functions, it can be replaced by variables with clearer meanings, such as T. run, also and let.

Make a comparison of the differences between these functions:

函数名称 是否作为扩展函数 是否返回对象本身 在函数内部使用this/ it
run no no -
T.run yes no it
with no no this
apply yes yes this
also yes yes it
let yes no it

Summarize


Related articles: