Chào mừng bạn đến với Cafedev, nơi chúng tôi chia sẻ kiến thức và đam mê về lập trình. Trong hành trình khám phá công nghệ, chúng ta không thể bỏ qua Kotlin, ngôn ngữ lập trình đa nhiệm và hiện đại. Bài viết này sẽ đưa bạn vào thế giới của Kotlin với những khái niệm cao cấp như Higher-order functions và lambdas. Hãy cùng Cafedev trải nghiệm sức mạnh của Kotlin, nơi mà mã nguồn trở nên linh hoạt và ngắn gọn. Mọi hành trình bắt đầu từ đây, cùng chúng tôi khám phá!

Các hàm trong Kotlin là first-class, có nghĩa là chúng có thể được lưu trữ trong biến và cấu trúc dữ liệu, và có thể được truyền làm tham số đầu vào và trả về từ các higher-order functions khác. Bạn có thể thực hiện bất kỳ thao tác nào trên các hàm mà cũng có thể thực hiện được với các giá trị không phải là hàm.
Để hỗ trợ điều này, Kotlin, là một ngôn ngữ lập trình có kiểu tĩnh, sử dụng một họ các function types để biểu diễn các hàm, và cung cấp một tập hợp các cấu trúc ngôn ngữ chuyên biệt, như lambda expressions.

1. Higher-order functions

Một hàm higher-order là một hàm nhận các hàm làm tham số hoặc trả về một hàm.
Một ví dụ tốt về hàm higher-order là fold functional dành cho bộ sưu tập. Nó nhận một giá trị tích luỹ ban đầu và một hàm kết hợp, và xây dựng giá trị trả về bằng cách kết hợp liên tiếp giá trị tích luỹ hiện tại với mỗi phần tử của bộ sưu tập, thay thế giá trị tích luỹ mỗi lần:

fun <T, R> Collection<T>.fold(
    initial: R,
    combine: (acc: R, nextElement: T) -> R
): R {
    var accumulator: R = initial
    for (element: T in this) {
        accumulator = combine(accumulator, element)
    }
    return accumulator
}

Trong đoạn mã trên, tham số combinefunction type (R, T) -> R, vì vậy nó chấp nhận một hàm nhận hai đối số kiểu RT và trả về một giá trị kiểu R. Nó được invoke bên trong vòng for, và giá trị trả về sau đó được gán cho accumulator.
Để gọi fold, bạn cần truyền một instance of the function type làm đối số, và các biểu diễn lambda (được mô tả chi tiết hơn ở dưới) thường được sử dụng cho mục đích này tại các điểm gọi hàm higher-order:

val items = listOf(1, 2, 3, 4, 5)

// Lambdas are code blocks enclosed in curly braces.
items.fold(0, { 
    // When a lambda has parameters, they go first, followed by '->'
    acc: Int, i: Int -> 
    print("acc = $acc, i = $i, ") 
    val result = acc + i
    println("result = $result")
    // The last expression in a lambda is considered the return value:
    result
})

// Parameter types in a lambda are optional if they can be inferred:
val joinedToString = items.fold("Elements:", { acc, i -> acc + " " + i })

// Function references can also be used for higher-order function calls:
val product = items.fold(1, Int::times)

2. Function types

Kotlin sử dụng function types, như (Int) -> String, cho các khai báo liên quan đến hàm: val onClick: () -> Unit = ....
Các loại này có một ký hiệu đặc biệt tương ứng với các chữ ký của các hàm – tham số và giá trị trả về của chúng:

  • Tất cả các loại hàm đều có một danh sách được đặt trong dấu ngoặc có kiểu tham số và kiểu trả về: (A, B) -> C biểu thị một loại đại diện cho các hàm nhận hai đối số kiểu AB và trả về một giá trị kiểu C. Danh sách kiểu tham số có thể trống, như trong () -> A. Kiểu trả về Unit không thể được bỏ qua.
  • Loại hàm có thể tùy chọn có một loại receiver bổ sung, được chỉ định trước dấu chấm trong ký hiệu: loại A.(B) -> C biểu thị các hàm có thể được gọi trên một đối tượng receiver A với tham số B và trả về một giá trị C. Hàm chữ literals với receiver thường được sử dụng cùng với các loại này.
  • Hàm treo thuộc một loại đặc biệt của loại hàm có suspend modifier trong ký hiệu của chúng, như suspend () -> Unit hoặc suspend A.(B) -> C.
    Ký hiệu loại hàm có thể tùy chọn bao gồm tên cho các tham số hàm: (x: Int, y: Int) -> Point.
    Các tên này có thể được sử dụng để tài liệu hóa ý nghĩa của các tham số.
    Để chỉ định rằng một loại hàm là nullable, sử dụng dấu ngoặc như sau: ((Int, Int) -> Int)?.
    Loại hàm cũng có thể được kết hợp bằng cách sử dụng dấu ngoặc: (Int) -> ((Int) -> Unit).

Ký hiệu mũi tên là kết hợp theo chiều từ phải sang trái, (Int) -> (Int) -> Unit tương đương với ví dụ trước, nhưng không tương đương với ((Int) -> (Int)) -> Unit.
Bạn cũng có thể đặt tên thay thế cho loại hàm bằng cách sử dụng một type alias:

typealias ClickHandler = (Button, ClickEvent) -> Unit

2.1 Khởi tạo một loại hàm

Có một số cách để có được một thể hiện của một loại hàm:
Có một số cách để có một phiên bản của một loại hàm:
* Sử dụng một khối mã trong một hàm literal, trong một trong các dạng sau:

một biểu thức lambda: { a, b -> a + b },

– một hàm ẩn danh: fun(s: String): Int { return s.toIntOrNull() ?: 0 }
Hàm người nhận có thể được sử dụng như giá trị của loại hàm với người nhận.
* Sử dụng một tham chiếu có thể gọi đến một khai báo hiện tại:

một hàm ở cấp độ cao, cục bộ, thành viên, hoặc mở rộng: ::isOdd, String::toInt,

– một thuộc tính ở cấp độ cao, thành viên, hoặc mở rộng: List::size,

một constructor: ::Regex
Bao gồm các tham chiếu có thể gọi đến ràng buộc trỏ đến một thành viên của một thể hiện cụ thể: foo::toString.
* Sử dụng các thể hiện của một lớp tùy chỉnh thực hiện một loại hàm như một giao diện:

class IntTransformer: (Int) -> Int {
    override operator fun invoke(x: Int): Int = TODO()
}

val intFunction: (Int) -> Int = IntTransformer()

Trình biên dịch có thể suy luận các loại hàm cho biến nếu có đủ thông tin:

val a = { i: Int -> i + 1 } // The inferred type is (Int) -> Int

Các Non-literal giá trị của loại hàm với và không có người nhận có thể thay thế cho nhau, vì vậy người nhận có thể đứng ở vị trí của tham số đầu tiên, và ngược lại. Ví dụ, một giá trị của loại (A, B) -> C có thể được chuyển hoặc gán vào nơi mong đợi một giá trị của loại A.(B) -> C, và ngược lại:

val repeatFun: String.(Int) -> String = { times -> this.repeat(times) }
val twoParameters: (String, Int) -> String = repeatFun // OK

fun runTransformation(f: (String, Int) -> String): String {
    return f("hello", 3)
}
val result = runTransformation(repeatFun) // OK

Một loại hàm không có người nhận được suy luận mặc định, ngay cả khi một biến được khởi tạo với một tham chiếu đến một hàm mở rộng. Để thay đổi điều đó, chỉ định rõ kiểu biến.

2.2 Gọi một phiên bản của loại hàm

Một giá trị của loại hàm có thể được gọi bằng cách sử dụng toán tử invoke(…) của: f.invoke(x) hoặc chỉ là f(x).
Nếu giá trị có một loại người nhận, đối tượng người nhận nên được chuyển làm tham số đầu tiên. Một cách khác để gọi một giá trị của loại hàm với người nhận là thêm đối tượng người nhận vào đầu, như nếu giá trị đó là một hàm mở rộng: 1.foo(2).
Ví dụ:

val stringPlus: (String, String) -> String = String::plus
val intPlus: Int.(Int) -> Int = Int::plus

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

println(intPlus.invoke(1, 1))
println(intPlus(1, 2))
println(2.intPlus(3)) // extension-like call

2.3 Hàm nội tuyến

Đôi khi việc sử dụng hàm nội tuyến, cung cấp luồng kiểm soát linh hoạt, là hữu ích cho các hàm bậc cao.

3. Biểu thức lambda và hàm ẩn danh

Biểu thức lambda và hàm ẩn danh là hàm literal. Hàm literal là các hàm không được khai báo nhưng được chuyển ngay lập tức như một biểu thức. Xem ví dụ sau:

max(strings, { a, b -> a.length < b.length })

Hàm max là một hàm bậc cao, vì nó lấy một giá trị hàm làm tham số thứ hai. Tham số thứ hai này là một biểu thức là chính nó một hàm, được gọi là hàm literal, tương đương với hàm có tên sau đây:

fun compare(a: String, b: String): Boolean = a.length < b.length

3.1 Cú pháp biểu thức lambda

Cú pháp đầy đủ của biểu thức lambda như sau:

val sum: (Int, Int) -> Int = { x: Int, y: Int -> x + y }

* Một biểu thức lambda luôn được bao quanh bởi dấu ngoặc nhọn.
* Khai báo tham số trong cú pháp đầy đủ được đặt trong dấu ngoặc nhọn và có thể có chú thích kiểu tùy chọn.
* Phần thân được đặt sau ->.
* Nếu kiểu trả về suy luận của lambda không phải là Unit, biểu thức cuối cùng (hoặc có thể là duy nhất) bên trong thân lambda được xem xét là giá trị trả về.
Nếu bỏ qua tất cả các chú thích tùy chọn, cái còn lại sẽ trông như thế này:

val sum = { x: Int, y: Int -> x + y }

3.2 Chuyển biểu thức lambda cuối cùng

Theo quy ước của Kotlin, nếu tham số cuối cùng của một hàm là một hàm, thì một biểu thức lambda được chuyển làm đối số tương ứng có thể được đặt bên ngoài dấu ngoặc đơn:

val product = items.fold(1) { acc, e -> acc * e }

Cú pháp như vậy còn được gọi là trailing lambda.
Nếu lambda là đối số duy nhất trong cuộc gọi đó, dấu ngoặc có thể được bỏ qua hoàn toàn:

run { println(“…”) }

3.3 it: tên ngầm định của một tham số duy nhất

Rất phổ biến khi biểu thức lambda chỉ có một tham số.
Nếu trình biên dịch có thể phân tích cú pháp mà không có bất kỳ tham số nào, thì không cần phải khai báo tham số và -> có thểđược bỏ qua. Tham số sẽ được khai báo ngầm định dưới tên it:

ints.filter { it > 0 } // this literal is of type ‘(it: Int) -> Boolean’

3.4 Trả về giá trị từ biểu thức lambda

Bạn có thể trả giá trị một cách rõ ràng từ lambda bằng cách sử dụng cú pháp return. Nếu không, giá trị của biểu thức cuối cùng sẽ được trả ngầm định.
Do đó, hai đoạn mã sau đây là tương đương nhau:

ints.filter {
    val shouldFilter = it > 0
    shouldFilter
}

ints.filter {
    val shouldFilter = it > 0
    return@filter shouldFilter
}

Quy ước này, cùng với chuyển biểu thức lambda bên ngoài dấu ngoặc đơn, cho phép mã theo kiểu LINQ-style:

strings.filter { it.length == 5 }.sortedBy { it }.map { it.uppercase() }

3.5 Dấu gạch dưới cho biến không sử dụng

Nếu tham số lambda không được sử dụng, bạn có thể đặt một dấu gạch dưới thay vì tên của nó:

map.forEach { (_, value) -> println(“$value!”) }

3.6 Phá hủy trong biểu thức lambda

Phá hủy trong biểu thức lambda được mô tả như là một phần của khai báo phá hủy.

3.7 Hàm ẩn danh

Cú pháp biểu thức lambda ở trên thiếu một điều – khả năng chỉ định kiểu trả về của hàm. Trong hầu hết các trường hợp, điều này không cần thiết vì kiểu trả về có thể được suy luận tự động. Tuy nhiên, nếu bạn cần chỉ định nó một cách rõ ràng, bạn có thể sử dụng cú pháp thay thế: một hàm ẩn danh.

fun(x: Int, y: Int): Int = x + y

Một hàm ẩn danh trông rất giống như một khai báo hàm thông thường, ngoại trừ tên của nó bị bỏ qua. Thân của nó có thể là một biểu thức (như đã hiển thị ở trên) hoặc một khối:

fun(x: Int, y: Int): Int {
return x + y
}

Các tham số và kiểu trả về được chỉ định theo cách tương tự như cho hàm thông thường, ngoại trừ kiểu tham số có thể bị bỏ qua nếu chúng có thể được suy luận từ ngữ cảnh:

ints.filter(fun(item) = item > 0)

Sự suy luận kiểu trả về cho hàm ẩn danh hoạt động giống như cho hàm thông thường: kiểu trả về được suy luận tự động cho hàm ẩn danh với thân biểu thức, nhưng phải được chỉ định rõ ràng (hoặc giả định là Unit) cho các hàm ẩn danh với thân khối.

Khi chuyển hàm ẩn danh như tham số, đặt chúng trong dấu ngoặc đơn. Cú pháp rút gọn cho phép bạn để hàm bên ngoài dấu ngoặc đơn chỉ hoạt động cho biểu thức lambda.
Một khác biệt khác giữa biểu thức lambda và hàm ẩn danh là hành vi của trả về không cục bộ. Một câu lệnh return mà không có nhãn luôn trả về từ hàm được khai báo với từ khóa fun. Điều này có nghĩa là một return trong một biểu thức lambda sẽ trả về từ hàm bao quanh, trong khi một return trong một hàm ẩn danh sẽ trả về từ chính hàm ẩn danh đó.

3.8 Closures

Một biểu thức lambda hoặc hàm ẩn danh (cũng như hàm cục bộđối tượng biểu thức) có thể truy cập vào đóng của nó, bao gồm các biến được khai báo trong phạm vi bên ngoài. Các biến bị giữ trong đóng có thể được sửa đổi trong lambda:

var sum = 0
ints.filter { it > 0 }.forEach {
sum += it
}
print(sum)

3.9 Hàm literal với người nhận

Loại hàm với người nhận, như A.(B) -> C, có thể được khởi tạo bằng một dạng đặc biệt của hàm literal – hàm literal với người nhận.
Như đã đề cập ở trên, Kotlin cung cấp khả năng gọi một thể hiện của một loại hàm với người nhận trong khi cung cấp đối tượng người nhận.
Bên trong thân hàm literal, đối tượng người nhận được chuyển đến cuộc gọi trở thành một this ngầm định, để bạn có thể truy cập các thành viên của đối tượng người nhận mà không cần bất kỳ bổ sung nào, hoặc truy cập đối tượng người nhận bằng cách sử dụng một biểu thức this.
Hành vi này tương tự như hàm mở rộng, cũng cho phép bạn truy cập các thành viên của đối tượng người nhận trong thân hàm.
Dưới đây là một ví dụ về một hàm literal với người nhận cùng với kiểu của nó, trong đó plus được gọi trên đối tượng người nhận:

val sum: Int.(Int) -> Int = { other -> plus(other) }

Cú pháp hàm ẩn danh cho phép bạn chỉ định trực tiếp kiểu của đối tượng người nhận của một hàm literal. Điều này có thể hữu ích nếu bạn cần khai báo một biến của loại hàm với người nhận, và sau đó sử dụng nó sau này.

val sum = fun Int.(other: Int): Int = this + other

Biểu thức lambda có thể được sử dụng như hàm literal với người nhận khi kiểu người nhận có thể được suy luận từ ngữ cảnh. Một trong những ví dụ quan trọng nhất về cách chúng được sử dụng là người xây dựng an toàn kiểu:

class HTML {
    fun body() { ... }
}

fun html(init: HTML.() -> Unit): HTML {
    val html = HTML()  // create the receiver object
    html.init()        // pass the receiver object to the lambda
    return html
}

html {       // lambda with receiver begins here
    body()   // calling a method on the receiver object
}

Cảm ơn bạn đã dành thời gian đọc bài viết về Kotlin với Higher-order functions và lambdas trên Cafedev. Chúng tôi hy vọng rằng bạn đã có những phút giây thú vị và bổ ích. Nếu có bất kỳ thắc mắc hay ý kiến đóng góp nào, đừng ngần ngại chia sẻ trên Cafedev, nơi mà cộng đồng lập trình viên gặp gỡ và trao đổi kiến thức. Hãy tiếp tục đồng hành cùng Cafedev, nơi chúng ta cùng nhau khám phá và đổi mới trong thế giới lập trình.”

Các nguồn kiến thức MIỄN PHÍ VÔ GIÁ từ cafedev tại đây

Nếu bạn thấy hay và hữu ích, bạn có thể tham gia các kênh sau của Cafedev để nhận được nhiều hơn nữa:

Chào thân ái và quyết thắng!

Đăng ký kênh youtube để ủng hộ Cafedev nha các bạn, Thanks you!