Kotlin standard functions: run-with-let-also-apply

Giới thiệu

Standard.kt là một phần nằm trong Kotlin library. Trong đó định nghĩa một số hàm mở rộng như let, apply, run with also mà chắc hẳn bạn đã từng nhìn thấy trong các guide hay đã từng sử dụng chúng.

Trong bài biết này, ta sẽ cùng tìm hiểu cách để phân biệt sự khác nhau giữa chúng và khi nào thì sử dụng nhé 😄

Scoping Functions

Một số function mà mình focus vào là run, with, T.run, T.let, T.alsoT.apply. Ta gọi đó là những scoping function (hàm phạm vi) bởi chúng có chức năng chính là cung cấp scope nhỏ hơn từ hàm gọi nó.

Bên dưới là một ví dụ về scoping function :

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

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

    println(mood)  // I am sad
}

Với ví dụ này thì bạn có thể có một scope riêng biệt với giá trị mood được định nghĩa lại thành I am happy và nó chỉ tồn tại bên trong run. Một hữu ích khác của run là nó trả về đối tượng cuối cùng nằm trong scope.

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

Dễ thấy sử dụng run sẽ ngắn gọn hơn nhiều so với cách thông thường,

3 attributes of scoping functions

Để phân loại các scoping funtion, ta sẽ dựa vào những tiêu chí sau :

  • Normal vs. extension function
  • This vs. it argument
  • Return this vs. other type

1. Normal vs. extension function

Nếu ta để ý withT.run sẽ thấy 2 hàm này khá là giống nhau. Ví dụ bên dưới cho ra kết quả tương đương nhau :

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

Tuy nhiên, khác nhau giữa chúng là một thằng là normal function (with), một thằng là extension function (T.run) Vậy khi nào thì sử dụng with, khi nào thì sử dụng T.run ?

Hãy tưởng tượng nếu webview.settings có thể null, ví dụ bên trên sẽ được viết lại như sau :

// Yack!
with(webview.settings) {
      this?.javaScriptEnabled = true
      this?.databaseEnabled = true
   }
}
// Nice.
webview.settings?.run {
    javaScriptEnabled = true
    databaseEnabled = true
}

Trong trường hợp này, T.run tốt hơn, bởi ta có thể check null trước khi sử dụng nó.

2. This vs. it argument

Tiếp theo là T.runT.let, cả 2 function khá giống nhau ngoại trừ cách mà chúng nhận tham số.

Hãy nhìn ví dụ bên dưới :

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

Nếu để ý vào chi tiết của T.run, bạn sẽ thấy T.run chỉ được tạo ra dưới dạng extension function gọi là T.() . Do đó tất cả bên trong scope, T có thể được gọi như this. Trong lập trình, phần lớn this đều có thể bỏ qua. Vì vậy, ta có thể sử dụng $length thay vì ${this.length}.

Với T.let, bạn sẽ thấy T.let gửi chính nó vàoblock: (T). và có thể được gọi trong function như it.

Từ đặc điểm củaT.letT.run, có vẻ như T.run tốt hơn T.let, nhưng T.let có một số ưu điểm như sau :

  • T.let giúp phân biệt rõ ràng hơn biến thành viên và bên ngoài
  • Trong 1 số trường hợp this không thể bỏ qua, “it” ngắn hơn “this”
  • T.let cho phép đặt tên chuyển đổi, do đó bạn có thể đổi “it” sang một cái tên khác dễ hiểu hơn.
stringVariable?.let {
      nonNullString ->
      println("The non null string is $nonNullString")
}

3.Return this vs. other type

Giờ hãy để ý đến T.letT.also, cả 2 đều giống nhau nếu ta nhìn vào phần thân của chúng :

stringVariable?.let {
      println("The length of this String is ${it.length}")
}
// Exactly the same as below
stringVariable?.also {
      println("The length of this String is ${it.length}")
}

Tuy nhiên, sự khác biệt giữa chúng là đối tượng trả về. T.let cho phép trả về một kiểu giá trị khác trong khi T.also trả về chính nó T.

Cả 2 đều hữu ích đối với các hàm chuỗi (chaining function) khi mà T.let cho phép bạn triển khai các operation liên tục và T.also cho phép bạn biểu diễn trên cùng một variable. Dưới đây là một minh họa :


val original = "abc"
// Evolve the value and send to the next chain
original.let {
    println("The original String is $it") // "abc"
    it.reversed() // evolve it as parameter to send to next let
}.let {
    println("The reverse String is $it") // "cba"
    it.length  // can be evolve to other type
}.let {
    println("The length of the String is $it") // 3
}
// Wrong
// Same value is sent in the chain (printed answer is wrong)
original.also {
    println("The original String is $it") // "abc"
    it.reversed() // even if we evolve it, it is useless
}.also {
    println("The reverse String is ${it}") // "abc"
    it.length  // even if we evolve it, it is useless
}.also {
    println("The length of the String is ${it}") // "abc"
}
// Corrected for also (i.e. manipulate as original string
// Same value is sent 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 có vẻ không có tác dụng ở trên, vì ta có thể kết hợp chúng thành một khối chức năng duy nhất. Tuy nhiên, suy nghĩ kĩ một chút, chúng ta có thể thấy nó có một số điểm mạnh như:

  • Tách rời rất rõ ràng trên cùng một đối tượng, tức là làm cho phần chức năng nhỏ hơn.
  • Có thể tự thao tác trước khi được sử dụng, tạo ra một hoạt động xây dựng chuỗi.

Thử kết hợp T.letT.also xem nào :

// Normal approach
fun makeDir(path: String): File  {
    val result = File(path)
    result.mkdirs()
    return result
}

// Improved approach
fun makeDir(path: String) = path.let{ File(it) }.also{ it.mkdirs() }

Looking at all attributes

Qua các tiêu chí trên, ta có thể thấy được khá đầy đủ về cách các function run with let also hoạt động. Vậy còn T.apply thì sao ?

  • T.apply là một extension function
  • T.apply gửi this như một tham số
  • T.apply trả về this.

Dưới đây là ví dụ :

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

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

Function selections

Dựa vào các đặc trưng và tính năng của từng loại mà bạn hãy quyết định sử dụng chúng một cách hợp lý nhé 😄

References

  1. https://medium.com/@elye.project/mastering-kotlin-standard-functions-run-with-let-also-and-apply-9cd334b0ef84