Functional Programming’s Toolkit: Map

Greek_lambda

I. Giới thiệu

Tiếp theo trong chuỗi các bài về các pattern hay gặp trong Functional Programming, bài viết này sẽ đề cập tới một công cụ khác là hàm Map. So với Fold thì đây là một hàm mà các lập trình viên quen thuộc hơn và có trong thư viện chuẩn của rất nhiều ngôn ngữ lập trình (Python, Ruby v.v..)

II. Định nghĩa và tính chất của map trong Scala

Tương tự như Fold, Map được định nghĩa trong trait TraversableLike với signature như sau:

trait TraversableLike[+A, +Repr] 
  def map[B, That](f: A => B)(implicit bf: CanBuildFrom[Repr, B, That]): That

Tạm thời chúng ta bỏ qua tham số implicit bf, rút gọn lại sẽ có signature như sau:

trait TraversableLike[+A, +Repr] 
  def map[B, That](f: A => B): That

Như vậy nếu chúng ta có một Collection c chứa 1 type [A], thì việc gọi c.map với tham số là một hàm f sẽ trả về một Collection khác chứa type là [That], That được quyết định theo kiểu trả về của f. Một vài ví dụ:

val c = 1 to 10
// c: scala.collection.immutable.Range.Inclusive = Range(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)

c map (_ + 1)
// res1: scala.collection.immutable.IndexedSeq[Int] = Vector(2, 3, 4, 5, 6, 7, 8, 9, 10, 11)

c map (_ * 2)
// res2: scala.collection.immutable.IndexedSeq[Int] = Vector(2, 4, 6, 8, 10, 12, 14, 16, 18, 20)

c map (_ toString)
// res3: scala.collection.immutable.IndexedSeq[String] = Vector(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)

c map (_ * 1.2)
// res4: scala.collection.immutable.IndexedSeq[Double] = Vector(1.2, 2.4, 3.59, 4.8, 6.0, 7.199, 8.4, 9.6, 10.79, 12.0)

Chúng ta có thể để ý vài điểm sau đây:

1. Cấu trúc của collection c được giữ nguyên là IndexedSeq, cả class Range lẫn Vector đều implement interface này. Vậy việc gọi map trên một collection không làm thay đổi cấu trúc của collection đó.

2. Kiểu trả về của đối tượng chứa trong collection phụ thuộc vào kiểu của hàm xử lý f:

_ + 1: Int => Int
_ * 2: Int => Int
_ toString: Any => String
_ * 1.2: Int => Float

Và kiểu tương ứng của collection sau khi map chính là kiểu tương đương.

3. Đến đây sẽ có thể có người sẽ thắc mắc là tại sao type ban đầu của collection là Range nhưng sau khi map lại ra Vector. Nguyên nhân chính là do tham số implicit bf ghi ở trên trong signature của hàm:

implicit bf: CanBuildFrom[Repr, B, That]

Ở đây Scala đã sử dụng implicit để có thể điều chỉnh được type của cả collection đầu ra cũng như đầu vào. Bằng cách này Scala sẽ tự biết lựa chọn kiểu collection nào phù hợp nhất với hàm xử lý. Như chúng ta có thể thấy trong IndexedSeq.scala:

/** $factoryInfo
* The current default implementation of a $Coll is a `Vector`.
* @define coll indexed sequence
* @define Coll `IndexedSeq`
*/
object IndexedSeq extends IndexedSeqFactory[IndexedSeq] {
  implicit def canBuildFrom[A]: CanBuildFrom[Coll, A, IndexedSeq[A]] =
    ReusableCBF.asInstanceOf[GenericCanBuildFrom[A]]
...
}

Với một collection chúng ta có thể định nghĩa một trường hợp mặc định cho output collection và những trường hợp đặc biệt hơn. Đây là một đặc điểm của Scala Collection, Scala sẽ chọn được Collection type nào tối ưu nhất cho kiểu dữ liệu được chứa bên trong.

scala> val bits = BitSet(1, 2, 3)
bits: scala.collection.immutable.BitSet = BitSet(1, 2, 3)

scala> bits map (_ * 2)
res13: scala.collection.immutable.BitSet = BitSet(2, 4, 6)

scala> bits map (_.toFloat)
res14: scala.collection.immutable.Set[Float]
= Set(1.0, 2.0, 3.0)

Như trong đoạn code trên, kiểu BitSet được giữ nguyên khi index type là Int và không trùng nhau, còn khi chuyển sang type khác không phải là Int thì BitSet sẽ tự động được convert sang generic type là Set.

III. Sử dụng map

Việc sử dụng map rất đơn giản, chúng ta sử dụng map để ‘biến đổi’ giá trị chứa trong một collection trong khi giữ nguyên cấu trúc của collection đó. Việc sử dụng map so với cách lặp từng phần tử thông thường có một số ưu điểm sau:

  1. Trừu tượng hóa, tách biệt cấu trúc của collection với thao tác trên từng phần tử: Chúng ta không cần quan tâm một collection là Vector, Array, Set, Option v.v… mà chỉ cần quan tâm đến việc chúng ta sẽ xử lý từng phần tử như thế nào.
  2. Tăng khả năng tái sử dụng code: là một kết quả của việc trừu tượng hóa trên, sau khi đã build hàm f chúng ta có thể sử dụng trên nhiều collection khác nhau.
  3. Dễ hiểu: Bằng việc tách biệt việc “xử lý từng phần tử” và “cách duyệt danh sách phần tử” thay vì chập chúng vào một vòng lặp, người lập trình viên sẽ được ‘giảm tải’ tránh phải giải quyết quá nhiều vấn đề cùng một thời điểm.
  4. Hiệu năng: Bằng cách xây dựng 1 chuỗi các thao tác xử lý, chúng ta đã tránh việc phải lặp lại nhiều lần trên cùng một collection. Hơn nữa cách này cũng giúp cho JVM dễ tối ưu hóa code của chúng ta hơn do chỉ phải xử lý hàm f.

IV. Reference:

  • http://docs.scala-lang.org/overviews/core/architecture-of-scala-collections.html

Add a Comment

Scroll Up