Tìm hiểu về high-order function trong kotlin

I, High-order function

  • Các kotlin function đều là first class: nó có thể trở thành kiểu dữ liệu cho variable, tham số hơặc kết quả return của high-order function.
  • Ta gọi đó là function type (kiểu dữ liệu function).
  • High-order function là function lấy function khác làm tham số hoặc trả về 1 function.
  • Chú ý: high-order function sẽ khiến tạo thêm các function type object trong memory. Do đó để tối ưu bộ nhớ, chúng ta có thể sử dụng inline function.
  • Để hiểu rõ hơn, chúng ta sẽ đi phân tích high-order function fold():
  • Ví dụ 1:
1
2
3
4
5
inline fun <T, R> Iterable<T>.fold(initial: R, operation: (R, T) -> R): R {
var accumulator = initial
for (element in this) accumulator = operation(accumulator, element)
return accumulator
}
  • Hàm fold() nhận 2 tham số:
    • 1, initial: có kiểu là R
    • 2, operation: có kiểu là (R, T) -> R ? (R, T) -> R là 1 function type. Do đó nó cũng có thể là kiểu dữ liệu của tham số operation trong high-order function fold().
  • Ta có (R, T) -> R định nghĩa cho 1 function type có:
    • 1, Hai tham số là với kiểu dữ liệu là RT.
    • 2, Giá trị trả về có kiểu dữ liệu là R.

II, Function types

  • Ở ví dụ 1, (R, T) -> R là function type nên nó cũng như các kiểu dữ liệu thông thường khác: val operation: (R, T) -> R = ...
  • Khi sử dụng function type, bạn cần chú ý:
    • 1, Function type đều có: danh sách kiểu dữ liệu các tham sốkiểu dữ liệu của giá trị trả về. Ví dụ (A, B) -> C có 2 tham số với kiểu dữ liệu lần lượt là A, B và kiểu dữ liệu của giá trị trả về là C.
    • 2, Danh sách kiểu dữ liệu của tham số có thể rỗng như () -> A. Kiểu của dữ liệu trả về phải được xác định dù đó có là Unit.
    • 3, Function type có thể có thêm kiểu dữ liệu của receiver. Ví dụ A.(B) -> C có kiểu dữ liệu của receiver là A, kiểu dữ liệu của tham số là B và kiểu dữ liệu của giá trị trả về là C.
    • 4, Function type có thể null, bạn chỉ cần thêm ()? vào nguyên mẫu. Ví dụ ((A, B) -> C)?.
    • 5, Suspending function là function có kiểu dữ liệu function đặc biệt (mình sẽ nói về công cụ tuyệt vời này sau), bạn phải thêm suspend keyword: suspend () -> A, suspend A.(B) -> C.
  • Chú ý: chúng ta có thể thêm tên cho các tham số như (x: Int, y: Int) -> Point. Nó giúp bạn cho bạn viết document thuận tiện hơn.
  • Ví dụ 2: chúng ta thêm tên cho các tham số cho kiểu dữ liệu của operation trong hàm fold() ở ví dụ 1
1
2
3
4
// It's ok to name for `R` type with `acc`.
inline fun <T, R> Iterable<T>.fold(initial: R, operation: (acc: R, T) -> R): R {
...
}
  • Ví dụ 3: kiểu dữ liệu function nhận thêm receiver thì this đại diện cho receiver.
1
2
3
4
5
6
7
8
9
10
11
12
// Only one parameter in the function type so `it` is implicitly used.
val sum1: Int.(String) -> Int = {
print(this)
print(it)
4
}

// Two parameter in the function type so we must explicitly specify name of two.
val sum: Int.(String, String) -> Int = { s1, s2 ->
// ...
4
}

III, Khởi tạo kiểu function type

  • Để tạo ra 1 instance của function type, chúng ta có thể sử dụng: function literal hoặc callable reference.
  • Ngoài ra còn có cách khác là tạo custom class implement kiểu dữ liệu function như 1 interface. Bạn phải override invoke().

1, Function literal

  • Function literal bao gồm có:
    • Lambada expression có dạng {a, b -> a + b}
    • Anonymous function có dạng fun(s: String): Int { return s.toIntOrNull() ?: 0 }.
  • Ví dụ 4:
1
2
3
4
val list = listOf(1, 2, 3, 4)
list.filter { item -> item > 2 } // using lambada
// Or
list.filter(fun(item) = item > 2) // using anonymous function

2, Callable reference

  • Các callable reference đều thêm :: vào trước định danh của chúng.

  • Kiểu dữ liệu funtion có thể được khởi tạo với Callable reference bao gồm:

    • function reference có dạng ::isOdd, String::toInt
    • property reference có dạng ::x, List<Int>::size
    • constructor reference có dạng ::Foo
  • Ví dụ 5: sử dụng function reference khởi tạo function type in ra kết quả [1, 3]

1
2
3
4
5
6
fun isOdd(x: Int) = x % 2 != 0

fun main() {
val numbers = listOf(1, 2, 3)
print(numbers.filter(::isOdd))
}
  • Ví dụ 6: sử dụng property reference khởi tạo function type in ra kết quả [1, 2, 3]
1
2
3
4
fun main() {
val strs = listOf("a", "bc", "def")
println(strs.map(String::length))
}
  • Ví dụ 7: sử dụng constructor reference khởi tạo function type. Nó gọi constructor của Foo class trong để khởi tạo factory
1
2
3
4
5
6
7
class Foo

fun test(factory: () -> Foo) {
val x: Foo = factory()
}

test(::Foo)

III, Sử dụng variable của function type

  • Chúng ta có thể gọi variable f có kiểu dữ liệu function bằng:

    • Thông qua invoke()f.invoke().
    • Gọi trực tiếp là f()
  • Nếu f có thể null thì ta chỉ dùng được f?.invoke()

  • Đối với function type có receiver, chúng ta xem ví dụ 8.

  • Ví dụ 8:

1
2
3
4
5
6
7
8
9
10
11
val stringPlus: (String, String) -> String = String::plus
val intPlus: Int.(Int) -> Int = Int::plus

println(stringPlus.invoke("<-", "->"))
// Or
println(stringPlus("Hello, ", "world!"))

// Function type with receiver.
println(intPlus.invoke(1, 1))
println(intPlus(1, 2))
println(2.intPlus(3)) // extension-like call

IV, Function literal với receiver

  • Như trên mình có nói rõ, function type có thể được khởi tạo bởi function literal (labamda expression hoặc anonymous function).
  • Do đó, function type có receiver A.(B) -> C có thể được khởi tạo bằng function literal với recevier.
  • Nói cách khác, lambada expression hay anonymous function sẽ nhận this keyword đại diện cho receiver object.
  • Ví dụ 9: this đại diện cho receiver object nên plus() là 1 function được gọi bởi receiver object.
1
2
3
4
5
6
7
// Using lambada expression
val sum1: Int.(Int) -> Int = { other -> plus(other) }
// Or
val sum2: Int.(Int) -> Int = { other -> this.plus(other) }

// Using anonymous function. It allows you to specify receiver type.
val sum = fun Int.(other: Int): Int = this + other