Map, FlatMap,… trong Kotlin

by Nguyễn Văn Cường
19 views

Giới thiệu

Kotlin sinh ra extension function làm cho việc tuân thủ nguyên tắc open/close trở nên dễ dàng hơn. Với extension function ta không cần phải kế thừa từ một class khi muốn mở rộng chức năng.

Có thể thấy extension function là thứ rất mạnh trong kotlin, cũng nhờ nó mà collections trong kotlin trở lên mạnh mẽ và làm cho việc lập trình đơn giản hơn rất nhiều.

Bài viết này mình sẽ giới thiệu một số chức năng trong collections kotlin:

  1. filter
  2. map
  3. flatMap
  4. groupBy
  5. partition

1. filter

Chức năng này trả về một list mới gồm các phần tử thỏa mãn predicate.

inline fun <T> Iterable<T>.filter(
    predicate: (T) -> Boolean
): List<T>

Ví dụ trả về danh sách các số chẵn:

listOf(1,2,3,4,5).filter { number->
    number%2 == 0
}

2. map

Chức năng này giúp chuyển đổi một collection thành một list sau phép biến đổi hàm ánh xạ transform T -> R mà ta định nghĩa.

inline fun <T, R> Iterable<T>.map(
    transform: (T) -> R
): List<R>

Kiểu trả về của danh sách sau phép map phụ thuộc vào kiểu trả về của lambda transform.

Ví dụ: Cho một danh sách sinh viên, điểm gpa cuối khóa sẽ được sử dụng để xét làm giảng viên. Một sinh viên được làm giảng viên nếu GPA > 3.8. Trả về danh sách giảng viên.

Ta sẽ kết hợp filter và map để làm ví dụ xàm này.

data class Student(val name:String, val gpa: Float)

data class Teacher(val name:String)

val students = getStudents()
val teachers = students.filter { it.gpa > 3.8 }
        .map { Teacher(it.name) }

Có thể thấy dùng filter + map làm code ngắn gọn dễ hiểu hơn nhiều đúng không nào.

3. flatMap

Nhìn thằng này chả khác gì thằng map ngoài thêm prefix flat, với một thằng ngu english như mình sau khi dịch flat thì chị google cho mình kết quả là phẳng, phẳng như nào thì cùng xem tiếp nhé.

Đây là hàm flatMap

inline fun <T, R> Iterable<T>.flatMap(
    transform: (T) -> Iterable<R>
): List<R>

flatMap khác map duy nhất chỗ kiểu trả về của lambda transform là Iterable<R> thay vì R.

flatMap sẽ map mỗi phần tử kiểu T thành một Iterable<R> . Cùng xem cách mà flatMap hoạt động dưới đây ta sẽ hiểu sao lại phẳng.

Các list trả về sau khi mỗi phần tử transform sẽ được addAll vào một list duy nhất là destination. Nếu dùng map thì sẽ trả về một list mà các phần tử trong đó là các list có độ dài khác nhau.

public inline fun <T, R> Iterable<T>.flatMap(transform: (T) -> Iterable<R>): List<R> {
    //Trả về giá trị của hàm flatMapTo
    //transform là phép biến đổi T -> Iterable<R> mà ta truyền vào
    return flatMapTo(ArrayList<R>(), transform)
}

public inline fun <T, R, C : MutableCollection<in R>> Iterable<T>.flatMapTo(destination: C, transform: (T) -> Iterable<R>): C {
    //Duyệt tất cả phần tử trong danh sách ban đầu
    for (element in this) {
	//Với mỗi phần tử biến đổi thành một list bởi hàm transform
        val list = transform(element)
	//Thêm vào arrayList
        destination.addAll(list)
    }
    return destination
}

flatMap rất hữu ích khi muốn transform một list với quan hệ 1-N.

Ví dụ: Một ứng dụng nghe nhạc có chức năng cho người dùng chọn nhiều thể loại và trả về tất cả các bài hát trong các thể loại đó. Người dùng lại tài khoản vip nên mỗi bài hát lấy ra sẽ được chuyển định dạng thành vip.

Ta có thể thấy thể loại và bài hát có quan hệ 1-N ta nghĩ ngay đến flatMap.

data class Song(val name: String, val singer: String)

data class VipSong(val name: String, val singer: String)

data class Category(val name: String, val songs: List<Song>)
...
val categories = listOf<Category>()
val vipSongs = categories.flatMap { category ->
    category.songs.map { VipSong(it.name, it.singer) }
}

4. groupBy

Hàm này có chức năng nhóm các phần tử trong collection theo selector và trả về một map collection với các key theo selector. Ví dụ: Liệt kê các học sinh theo tên cuong -> {Student("cuong", id = 1)}

data class Grade(val math: Float, val physics: Float)

data class Student(val name: String, val grade: Grade)
...

val students = listOf(
        Student("Cuong1", Grade(1f,2f)),
        Student("Cuong2", Grade(3f,3f)),
        Student("Cuong3", Grade(1f,2f)),
        Student("Cuong5", Grade(1.2f,2.5f))
    )
print(students.groupBy {
    it.grade
})

Kết quả sẽ trả về một map collection:

{
	Grade(math=1.0, physics=2.0)=[Student(name=Cuong1, grade=Grade(math=1.0, physics=2.0)), Student(name=Cuong3, grade=Grade(math=1.0, physics=2.0))],
	Grade(math=3.0, physics=3.0)=[Student(name=Cuong2, grade=Grade(math=3.0, physics=3.0))], 
	Grade(math=1.2, physics=2.5)=[Student(name=Cuong5, grade=Grade(math=1.2, physics=2.5))]
}

5. Partition

Hàm này có chức năng phân chia collection thành hai list theo điều kiện, một list là thỏa mãn điều kiện, một list là không thỏa mãn điều kiện.

Ví dụ: Cần lấy danh sách sinh viên được tốt nghiệp và sinh viên chưa được tốt nghiệp

data class Student(val name: String, val gpa: Float, val credits: Int)

fun isGraduated(student: Student) = student.run { gpa>=2.5 && credits == 150}

fun main() {
    val students = listOf(
        Student("Lan", 4.0f, 140),
        Student("Phuong", 2.45f, 150),
        Student("MA", 2.7f, 150)
    )
    val (graduated, undergraduate) = students.partition(::isGraduated)
    println("""
        graduated: $graduated
        undergraduate: $undergraduate
    """.trimIndent())
}

Kết quả:

graduated: [Student(name=MA, gpa=2.7, credits=150)]
undergraduate: [Student(name=Lan, gpa=4.0, credits=140), Student(name=Phuong, gpa=2.45, credits=150)]

Kết luận

Các chức năng này giúp chúng ta giảm thiếu số lượng mã cũng như code clear hơn, nó cũng quan trọng và gặp thường xuyên khi làm việc với các thư viện reactive như Rx, flow coroutine.

Leave a Comment

* By using this form you agree with the storage and handling of your data by this website.

You may also like