Trong bài viết này chúng ta sẽ cùng nhau tìm hiểu về khái niệm For Expression
hoặc For Comprehesion
trong Scala, mà trong ngôn ngữ khác chúng thường quen với khái niệm For Loop
Scala developer tương đối quen thuộc với For Comprehension
nhưng hầu hết chúng ta sẽ tự hỏi For Comprehension
này hoạt động như thế nào. Cơ bản thì nó sẽ chuyển đổi một biểu thức thành công cụ map, flatMap. Nhiều lập trình viên chia For Expression thành 2 dạng sau:
For Loop – vòng lặp quen thuộc
for ( i <- generator ) { statements }
For Comprehension
for ( i <- generator; a = definition; if (filter)) yield a
Thực ra cả hai trường hợp trên đều giống nhau và chỉ khác nhau về cách chúng ta gọi và sử dụng. Do đó, for
trong scala là một expression bởi vì tất cả mọi hành vi của hàm đều là expression (biểu thức), còn lại là dựa trên cách chúng ta gọi và sử dụng.
For Expression
có 3 đặc tính
p <- persons // Một Generator n = p.name // Một Definition if(n startsWith "Some") // Một Filter
Làm thế nào chúng ta sử dụng For Expression trên các class collection tuỳ chỉnh hoặc wrapper tuỳ chỉnh giống như Option?
Chúng ta sẽ có một ví dụ như sau
import scala.collection.mutable.ArrayBuffer import scala.collection.mutable.ArrayBuffer case class CustomList[A](elements: A*) { val elems = new ArrayBuffer[A] elems ++= elements } val list = CustomList(1, 2, 3) for( i <- list) { println(i) }
Kết quả
<console>:14: error: value foreach is not a member of CustomList[Int] for( i <- list) { println(i) }
Chúng ta sẽ nhận được lỗi khi chạy như ở trên. Nhưng lỗi bên trên tương đối dễ hiểu, chúng ta có thể hiểu rằng hàm foreach đang bị thiếu, nguyên nhân là vì trình biên dịch không thể chuyển đổi for expression sang thành phương thức map, flatMap, withFilter và foreach.
Theo lỗi này, chúng ta đang sử dụng một cách đơn giản như for loop
nhưng nó hoạt động như for expression
. Nếu bạn muốn hiểu rõ hơn thì có thể dùng lệnh sau scalac-Xprint:all file.scala
để hiển thị thông tin khi trình biên dịch chuyển đổi for expression
thành các phương thức
Nếu bạn muốn sử dụng for expression cho các class hoặc wrapper của mình thì nó phải hỗ trợ các method sau:
def foreach(c: A => Unit): Unit def map[B](f: A => B): CustomList[B] def withFilter(p: A => Boolean): CustomList[A] def flatMap[B](f: A => CustomList[B]): CustomList[B]
Làm thế nào sử dụng một generator và in ra các giá trị?
Để thực hiện được việc này, chúng ta phải implement hàm foreach trong class collection
def foreach(c: A => Unit): Unit = { elems.foreach(c) }
Làm thế nào để sử dụng definition và generator cùng nhau?
Chúng ta phải implement phương thức map. Nếu không, map của chúng ta không thể sử dụng phần definition trong For Expression
def map[B](f: A => B): CustomList[B] = { val temp = elems.map(f) CustomList(temp: _*) }
Làm thế nào để sử dụng for/yield, chúng ta phải sử dụng phương thức nào?
Điều này dựa trên yêu cầu của bài toán. Nếu bạn chỉ sử dụng một generator thì sẽ chỉ có một phương thức map được yêu cầu, nhưng với nhiều generator, chúng ta phải thêm vào flatMap nếu không muốn bị lỗi.
Hãy xem ví dụ bên dưới đây với trường hợp sử dụng nhiều generator
def flatMap[B](f: A => CustomList[B]): CustomList[B] = { val temp = elems.map(f) flatternLike(temp) } def flatternLike[B](seq: Seq[CustomList[B]]): CustomList[B] = { val temp = new ArrayBuffer[B] for(i <- seq) { for(j <- i) { temp += j } } CustomList(temp: _*) }
Thông thường trong collection chúng ta sử dụng phương thức filter nhưng tại sao ở đây chúng ta sử dụng withFilter?
Bởi vì filter theo một quy tắc rõ ràng nhưng withFilter hoạt động theo cách lazy. Khi chúng ta filter, nó luôn luôn trả về một collection mới nhưng withFilter sử dụng tính toán lazy và kiểm tra từng phần tử một.
Bất cứ khi nào chúng ta muốn tạo một collection và sử dụng nó với for expression, chúng ta có cần implement tất cả các phương thức không?
Chúng ta không cần phải làm thế, nó sẽ dựa theo yêu cầu của bài toán
- Chỉ sử dụng giống for loop, không sử dụng yield – foreach
- Cần sử dụng một generator với yield – map
- Cần sử dụng nhiều generator mà không cần yield – foreach
- Cần sử dụng nhiều generator với yield – foreach, map và flatmap
- Cần sử dụng một generator với definition – map
- Cần sử dụng generator với filter mà không cần yield – withFilter
Sau đây là toàn bộ phần code của chúng ta
/* Custom Collection */ import scala.collection.mutable.ArrayBuffer case class CustomList[A](elements: A*) { val elems = new ArrayBuffer[A] elems ++= elements def foreach(c: A => Unit): Unit = { elems.foreach(c) } def map[B](f: A => B): CustomList[B] = { val temp = elems.map(f) CustomList(temp: _*) } def withFilter(p: A => Boolean): CustomList[A] = { val temp = elems.filter(p) CustomList(temp: _*) } def flatMap[B](f: A => CustomList[B]): CustomList[B] = { val temp = elems.map(f) flatternLike(temp) } def flatternLike[B](seq: Seq[CustomList[B]]): CustomList[B] = { val temp = new ArrayBuffer[B] for(i <- seq) { for(j <- i) { temp += j } } CustomList(temp: _*) } } object ForExpression extends App { val ints = CustomList(1, 2, 3) for( i <- ints) { println(i)} val values1: CustomList[Int] = for(i <- ints; a = (i + 1)) yield a println(s"Map Values: $values1") val values2 = for { i <- ints if(i % 2 == 0) } yield i println(s"With Filter Values: $values2") val values3: CustomList[Int] = for(i <- ints; j <- ints; a = (i + j + 1)) yield a println(s"FlatMap Values: $values3") }