Scala – lazy, stream, view

Trong bài viết này chúng ta sẽ cùng tìm hiểu về tính toán Lazy trong Scala. Chúng ta sẽ tăng tính hiệu quả cho ứng dụng của mình như thế nào?

Tính hiệu quả đạt được không chỉ là chạy mọi thứ nhanh hơn, mà bằng cách tránh những điều không nên thực hiện ngay từ đầu

Trong lập trình hàm, tính toán lazy nghĩa là hiệu quả. Lazy giúp chúng ta tách rời những miêu tả của biểu thức khỏi tính toán của biểu thức đó. Điều này mang đến cho chúng ta lợi ích vô cùng lớn – chúng ta có thể lựa chọn miêu tả một biểu thức lớn hơn những gì chúng ta cần, và sau đó chỉ thực hiện tính toán một phần của nó. Có rất nhiều cách để thực hiện tính toán Lazy trong scala bằng cách sử dụng từ khoá lazy, views, stream…

Ví dụ

scala> val num = 3
num: Int = 3

scala> lazy val lazyNum = 3
lazyNum: Int = 

scala> lazyNum
res0: Int = 3

Ở đây nếu bạn để ý, khi chúng ta định nghĩa một biến val với tên num, nó có giá trị là 3. Nhưng khi chúng ta định nghĩa một biến val khác với tên là lazyNum sử dụng từ khoá lazy ở đằng trước, nó sẽ không có giá trị bởi vì nó được tính toán lazy. Điểm khác nhau ở đây là val được thực hiện khi nó được định nghĩa, trong khi lazy val được thực hiện khi nó được truy cập lần đầu tiên. Do đó khi chúng ta sử dụng lại biến lazyNum, nó có giá trị là 3.

Ngược lại  với một phương thức (đang được định nghĩa với def), lazy val chỉ thực hiện  tính toán một lần và không bao giờ thực hiện lại nữa. Điều này vô cùng hữu ích khi một hoạt động nào đấy mất nhiều thời gian để thực hiện và khi nó là không chắc chắn nếu được sử dụng sau này.

Scala hỗ trợ 2 kiểu collection:

  • Strict Collection ví dụ List, Map, Set…
  • Non-Strict Collections ví dụ Streams

Strict collection nghĩa là chúng được tính toán eagerly (ngay lập tức) như là List, Set, Vector, Map …

Ví dụ chúng ta tạo một List với 10 phần từ, thì memory được cấp phát cho các phần tử này ngay lập tức

scala> val list = (1 to 10).toList
list: List[Int] = List(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)

Nhưng Non-Strict như là Stream, mặc định được tính toán lazy. Chúng được tính toán dựa trên yêu cầu. Ví dụ nếu chúng ta tạo một Stream với 10 phần tử, nó sẽ không được tạo

scala> val lazyList = (1 to 10).toStream
lazyList: scala.collection.immutable.Stream[Int] = Stream(1, ?)

Ở đây, phần tử đầu tiên được tính toán ngay lập tức, nhưng phần tử sau thì chưa được tính toán.

Stream

Stream là một collection giống như List, nhưng nó được tính toán lazy.  Đây là lý do tại sao mà chúng ta có thể có rất nhiều các phần tử trong một Stream. Trong Stream, các phần tử đang được tính toán dựa trên yêu cầu. Chúng ta tạo một List sử dụng toán tử cons ::, giống như thế, chúng ta xây dựng Stream sử dụng toán tử #::

Ví dụ

scala> val stream = 1 #:: 2 #:: 3 #:: Stream.empty
stream: scala.collection.immutable.Stream[Int] = Stream(1, ?)

Chúng ta tạo một Stream với 3 phần tử 1, 2 và 3

Vì Stream tính toán luôn phần tử đầu tiên, do đó phần tử đầu tiên của Stream được in ra còn các phần tử còn lại được tính toán lazy. Đó là lý do tại sao nó chưa được tính toán. Nó sẽ được tính toán dựa trên yêu cầu. Phương thức toStream có thể chuyển đổi bất kỳ collection nào thành Stream

Ví dụ, tìm 10 số nguyên tố đầu tiên sau 100

scala> def isPrime(number: Int) =
 number > 1 && !(2 to number - 1).exists(e => e % number == 0)
isPrime: (number: Int)Boolean

scala> def generatePrimes(starting: Int): Stream[Int] = {
  if(isPrime(starting))
   starting #:: generatePrimes(starting + 1)
  else
    generatePrimes(starting + 1)
}
generatePrimes: (starting: Int)Stream[Int]

scala> generatePrimes(100).take(10)
res8: scala.collection.immutable.Stream[Int] = Stream(100, ?)

generatePrimes có số lượng số nguyên tố vô hạn, và chúng ta chỉ quan tâm lấy 10 số nguyên tố lớn hơn 100. Khi chúng ta thử chạy phương thức này, nó vẫn không trả về kết quả cho chúng ta. Bởi vì Stream sẽ không tính toán cho tới khi nó không cần thiết nữa.

Làm thế nào lấy giá trị trong Stream?

Chúng ta hoặc là sử dụng phương thức force để lấy giá trị của Stream hoặc chúng ta sử dụng phương thức toList để lấy danh sách các số nguyên tố

scala> generatePrimes(100).take(10).force
res9: scala.collection.immutable.Stream[Int] = Stream(100, 101, 102, 103, 104, 105, 106, 107, 108, 109)

scala> generatePrimes(100).take(10).toList
res10: List[Int] = List(100, 101, 102, 103, 104, 105, 106, 107, 108, 109)

View

View là một kiểu collection đặc biệt để biểu diễn cho một số collection cơ bản nhưng nó sẽ thực hiện tất cả các phương thức một cách Lazy

Ví dụ

scala> (1 to 1000000000).filter(_ % 2 != 0).take(20).toList
java.lang.OutOfMemoryError: GC overhead limit exceeded

Ở đây chúng ta tạo một danh sách 1 triệu phần tử và lấy 20 số lẻ đầu tiên. Nhưng tôi nhận một lỗi OOM. Nhưng khi sử dụng view

scala> (1 to 1000000000).view.filter(_ % 2 != 0).take(20).toList
res2: List[Int] = List(1, 3, 5, 7, 9, 11, 13, 15, 17, 19, 21, 23, 25, 27, 29, 31, 33, 35, 37, 39)

Bây giờ chúng ta sẽ không thấy lỗi OOM xuất hiện, lý do là vì Stream không bao giờ cung cấp bộ nhớ cho tất cả 1 triệu phần tử. Bộ nhớ chỉ được cấp phép cho các phần tử cần thiết.

Sự khác nhau giữa Stream và View là lazy View sử dụng trong tính toán phương thức trong khi Stream là lazy ở sau cùng. Một Stream không có giá trị, nó chỉ sinh ra giá trị khi chúng ta yêu cầu nó. Nhưng nó có thể có giá trị trong trường hợp View. Bên cạnh đó Stream sẽ cache lại kết quả còn View thì không. Trong View, các phần tử được tính toán lại mỗi lần chúng được truy cập. Trong Stream, các phần tử được giữ lại khi được tính toán.

 

You May Also Like

About the Author: Nguyen Dinh Thuc

Leave a Reply

Your email address will not be published.