12/08/2018, 10:01

Immutable and Non strict - collections

Khi bắt đầu vào dự án scala, để đạt yêu cầu của công việc, tôi đã cố giải quyết các tasks của mình theo java thuần (hay là theo C). Trong vài lần cố làm refactor code , tận dụng sức mạnh của scala . Có một số khái niệm thường được nhắc tới trong các tài liệu tìm hiểu ,tạm chia làm 2 cặp : ...

Khi bắt đầu vào dự án scala, để đạt yêu cầu của công việc, tôi đã cố giải quyết các tasks của mình theo java thuần (hay là theo C).
Trong vài lần cố làm refactor code , tận dụng sức mạnh của scala . Có một số khái niệm thường được nhắc tới trong các tài liệu tìm hiểu ,tạm chia làm 2 cặp :
*Immutable and mutable.
*Strict and non-strict collections
Vậy chúng là gì , và nó có ý nghĩa thế nào trong scala ? !.
Immutable and mutable:
Định nghĩa
“Mutable” nghĩa là bạn có thể thay đổi giá trị của biến ( hay collection)
“Immutable” nghĩa là bạn không thể thay đổi giá trị của biến (tương đương final trong java -val trong scala). (và cũng không ai khác có thể thay đổi biến của bạn).
Cách khai báo immutable và mutable

Tạo một biến immutable val tebbien:Type =value
Tạo một biến mutable var tenbien:Type =value

Tại sao nên sử dụng immutable -mutable
Viết 1 method với java :

public void doSomething(Foo foo) {
  // do something here
}

Chúng ta có thể thay đổi lại giá trị của foo . NHƯNG hầu như không bao giờ chúng ta làm điều này. Hay sử dụng 1 biến immutable tránh những bug tiềm ẩn

public void doSomething(final Foo foo) {
  // do something here
}

Nhưng vấn đề gặp với Java là bạn khi bạn thêm từ final vào method của bạn khiên cho bạn code nhiều hơn và đoạn code trở nên rườm rà . Phần lớn các dev Java không quan tâm đến việc add final và chỗ biến là immutable.
Immutable trong scala
Trong scala, tư tưởng có sự thay đổi, đối với các function , từ khóa val được mặc định và ta không phải viết thêm.

scala> def addOne(i: Int): Int = { i + 1 }
foo: (i: Int)Int

Trong function này , biến i được khai báo là mặc định là val. Ta có thể xác nhận điều này khi cố thay đổi giá trị của i

scala> def addOne(i: Int): Int = { i += 1; i }
<console>:7: error: reassignment to val
         def addOne(i: Int): Int = { i += 1; i }

Trong cuốn sách “Programming in Scala” tác giả viết :

“Prefer vals, immutable objects, and methods without side effects. Reach for them first.” Có hai thanh phần liên quan đến “prefer immutable”

  • Immutable collections. Nên nghĩ đến việc sử dụng các chuỗi bất biến như List và Vector trước khi nghĩ đến sử dụng mutable ArayyBuffer .
  • Immutable variables . Nên nghĩ đến việc sử dụng từ khóa val trước var

Bằng cách viết code theo cách này , bạn sẽ loại bỏ được các bugs trong hệ thống. Điều này được các nhà phát triển scala là ưu tiên.

Thêm 1 ví dụ cho sự ưu tiên này chính là khi bạn khai báo 1 collection , nếu bạn không chỉ ra thư viện immport , scala sẽ mặc định bạn đang sử dụng 1 immutable colection trong gói scala.collection.immutable

*Strict and non-strict
Từ phiên bản 2.8 , collection có projection method, nó trả về 1 non-strict collections.
Từ scala 2.8+ , method đã được thay đổi thành view
Một cái tên khác của non-strict collections , có thể gọi là Lazy colection

Để hiểu rõ thêm về , chúng ta có 1 example về strict collection

scala> var x=0
x: Int = 0
scala> def inc = {
| x += 1
| x
| }
inc: Int
scala> var list = List(inc , inc , inc )
list: List[() => Int] = List(<function0>, <function0>, <function0>)
scala> list.map (
()).head
res0: Int = 1
scala> list.map (
()).head
res1: Int = 4
scala> list.map (
()).head
res2: Int = 7

Làm thế nào mỗi lần chúng ta call x được tăng lên 3 lần , dường như việc gọi đến head , scala đã duyệt qua tất cả các element trong list , điều này khá là tồi, nhất là khi bạn có một mảng nhiều phần tử .
Okay. Tiếp đén chúng ta sẽ có 1 ví dụ về non-strict

scala> var x=0
x: Int = 0
scala> def inc = {
| x += 1
| x
| }
inc: Int
scala> var list = List(inc , inc , inc )
list: List[() => Int] = List(<function0>, <function0>, <function0>)
scala> list.projection.map (
()).head
res0: Int = 1
scala> list.projection.map (
()).head
res1: Int = 2
scala> list.projection.map (
()).head
res2: Int = 3
scala> list.projection.map (_()).head
res3: Int = 4

Bạn có thể thấy khi view trả về 1 non-strict collection , mỗi lần call x , chỉ có 1 phần từ trong list được gọi đến. (so cool) . Việc này đẫn đến hiệu năng tốt của scala . Để hiểu hơn chúng ta sẽ xem 1 ví dụ trong java tương ứng .

Đến đây bạn có thể hiểu được vì sao gọi là lazy collections.

Trên đây là 1 số tìm hiểu của tôi về scala , việc hiểu rõ về ý định của các nhà phát triển scala giúp cho bạn refactor code tốt hơn , dễ hiểu và dễ maintain.

Trước khi kết thúc bài viết, tôi sẽ đưa 1 ví dụ về việc refactor code theo scala .
Tôi có 1 danh sách các số và cần check xem đã sắp sếp hay chưa .
Theo Java thuần

def isSorted(list:Seq[Double])(implicit sortType: Boolean = ASC): Boolean = {
if (list != null && list.size > 0) {
for(i <- 0 to list.size-1) {
if (sortType) {
if((i+1 < list.size) && (list(i) > list(i+1))) {
return false
}
} else {
if((i+1 < list.size) && (list(i) < list(i+1))) {
return false
}
}

}
}
return true
}

Chúng ta cần đến khá nhiều dòng code để thực hiện 1 chức năng nhỏ . Nếu chúng ta viết theo phong cách scala thì thế nào ?

def isSorted(list: Seq[Double])(implicit sortType: Boolean = ASC): Boolean = {
if (list != null && !list.isEmpty) {
if (sortType) {
return list.view.zip(list.tail).forall(x => x._1 <= x._2)
} else {
return list.view.zip(list.tail).forall(x => x._1 >= x._2)
}
}
return true
}

Bạn thấy việc code hoàn toàn ngắn gọn và clear.
Đây chỉ là 1 ví dụ nhỏ cho việc call và tái cấu trúc theo scala .
Nhưng cần lưu ý một số điểm có thể ảnh hưởng đến hiệu năng của các dòng code của bạn.
Good luck

0