Monad

Có rất nhiều khái niệm khác nhau trong functional programming như là  HOFs, closures, currying, partial application…rất là hữu ích trong lập trình viên ngày nay. Một trong những khái niệm rất là quan trọng khác trong của functional programming hiện nay là Monad. Monad là một trong những khái niệm đưa ra rất đơn giản và trực quan ngay lập tức bạn hiểu được nó, nhưng sẽ cực kỳ khó hiểu nếu bạn không thực sự bắt tay vào tìm hiểu nó. Ngày nay thì có rẩt nhiều bài viết hướng dẫn về Monad ở đây.

Map và Functor

Phương thức map là một trong những khái niệm đầu tiên chúng ta sẽ gặp khi bắt đầu functional programming. Nó là một higher order function trong rất nhiều collection và trong các loại container khác. Nào chúng ta hãy cùng xem map hoạt động như thế nào trong List

val x = (0 to 4).toList
// x: List[Int] = List(0, 1, 2, 3, 4)
val x2 = x map { x => x * 3 }
// x2: List[Int] = List(0, 3, 6, 9, 12)
val x3 = x map { _ * 3 }
// x3: List[Int] = List(0, 3, 6, 9, 12)
val x4 = x map { _ * 0.1 }
// x4: List[Double] = List(0.0, 0.1, 0.2, 0.30000000000000004, 0.4)

Ví dụ cuối cùng biểu diễn một List[T] được biến đổi thành List[S] nếu map được truyền vào một hàm T => S. Tất nhiên không có gì đặc biệt về List ở đây. Nó hoạt động tương tự với các kiểu collection khác như ví dụ sau đây về Vector

val xv = x.toVector
// xv: Vector[Int] = Vector(0, 1, 2, 3, 4)
val xv2 = xv map { _ * 0.2 }
// xv2: scala.collection.immutable.Vector[Double] = Vector(0.0, 0.2, 0.4, 0.6000000000000001, 0.8)
val xv3 = for (xi <- xv) yield (xi * 0.2)
// xv3: scala.collection.immutable.Vector[Double] = Vector(0.0, 0.2, 0.4, 0.6000000000000001, 0.8)

Để ý rằng ở đây chúng ta sử dụng For comprehension để tạo ra xv3 là chính xác tương đương với gọi hàm map để tạo ra xv2. Cú pháp for comprehension trong scala chỉ là cú pháp khác để gọi hàm map (For sẽ được scala chuyển đổi thành map, flatMap..). Ưu điểm của cú pháp này sẽ giúp code chúng ta trong rõ ràng trong các ví dụ phức tạp mà chúng ta sẽ sử dụng sau đây.

Có rất nhiều collection và các kiểu container khác hoạt động theo cách này. Bất kỳ kiểu được tham số hoá mà có phương thức map như thế này thì được là Functor. Từ quan điểm của một lập trình viên Scala, Functor có thể coi như là một trait như sau

trait F[T] {
  def map(f: T => S): F[S]
}

Chúng ta có thể coi F như là một Functor. Trong thực tế để đơn giản hơn hãy nghĩ về Functor như một type class. Cũng chú ý rằng để là một Functor theo đúng nghĩa, phương thức map phải hoạt động hợp lý, tức là nó phải thoả mãn được nguyên lý của Functor

FlatMap và Monad

Trong khi chúng ta có thể map các hàm trên các phần tử của Container, chúng ta ngay lập tức map các hàm sẽ tự trả về các giá trị của kiểu Container. Ví dụ chúng ta có thể map một hàm trả về một List trên các phần tử của một List như ví dụ bên dưới đây

val x5 = x map { x => List(x - 0.1, x + 0.1) }
// x5: List[List[Double]] = List(List(-0.1, 0.1), List(0.9, 1.1), List(1.9, 2.1), List(2.9, 3.1), List(3.9, 4.1))

Rõ ràng đây là trả về một List của List. Thỉnh thoảng chúng ta mong muốn kết quả trả về như thế, nhưng thông thường chúng ta muốn flatten nó để được một List duy nhất, ví dụ, sau đó chúng ta có thể map toàn các phần tử của kiểu căn cứ vào với map duy nhất. Chúng ta có thể lấy một List của List và sau đấy flatten. Pattern này là tương đối thông dụng khi xử lý map rồi sau đó flatten, đây là hoạt động cơ bản thường được biết trong Scala như là flatMap. Do đó đối với ví dụ của chúng ta, có thể sử dụng flatMap như sau:

val x6 = x flatMap { x => List(x - 0.1, x + 0.1) }
// x6: List[Double] = List(-0.1, 0.1, 0.9, 1.1, 1.9, 2.1, 2.9, 3.1, 3.9, 4.1)

Sự phổ biến của Pattern này trở nên rõ ràng hơn khi chúng ta bắt đầu nghĩ về cách lặp các phần tử trên nhiều collection. Giả sử bây giờ chúng ta có 2 danh sách x, y và chúng ta muốn lặp tất cả các cặp phần tử bao gồm một phần tử từ mỗi danh sách

val y = (0 to 12 by 2).toList
// y: List[Int] = List(0, 2, 4, 6, 8, 10, 12)
val xy = x flatMap { xi => y map { yi => xi * yi } }
// xy: List[Int] = List(0, 0, 0, 0, 0, 0, 0, 0, 2, 4, 6, 8, 10, 12, 0, 4, 8, 12, 16, 20, 24, 0, 6, 12, 18, 24, 30, 36, 0, 8, 16, 24, 32, 40, 48)

Pattern này có một hoặc nhiều flatMap lồng nhau, sau đó cuối cùng dùng map để lặp trên nhiều collection là tương đối thông dụng.  Đó là chính xác là những gì chúng ta xử lý khi dùng pattern này mà chúng ta có thể dùng tương tự for-comprehension. Nên chúng ta có thể viết lại đoạn code trên dùng for-comprehension

val xy2 = for {
  xi <- x
  yi <- y
} yield (xi * yi)
// xy2: List[Int] = List(0, 0, 0, 0, 0, 0, 0, 0, 2, 4, 6, 8, 10, 12, 0, 4, 8, 12, 16, 20, 24, 0, 6, 12, 18, 24, 30, 36, 0, 8, 16, 24, 32, 40, 48)

For-comprehension (thường được gọi là for expression trong scala) có cấu trúc trực quan hơn. Nhớ rằng <- không thực sự bắt buộc khi gán. For-comprehension thực sự là mở rộng cho pure-functional với flatmap lồng nhau và map gọi như ở trên.

Hãy nhớ rằng Functor là một kiểu được tham số hoá với phương thức map, hiện tại thì chúng ta có thể nói rằng Monad chỉ là một Functor mà có một phương thức là flatMap. Chúng ta có thể biểu diễn thành code như sau

trait M[T] {
  def map(f: T => S): M[S]
  def flatMap(f: T => M[S]): M[S]
}

Không phải tất cả Functor đều có hoạt động flatten, nên không phải tất cả Functor đều là Monad nhưng tất cả Monad đều là Functor, Monad ở đây có nhiều quyền hơn Functor. Tuy nhiên nhiều quyền hơn không phải lúc nào cũng là tốt.  Nguyên tắc least power là một trong những nguyên tắc chính của funtional programming, nhưng Monad là rất hữu dụng cho các chuỗi tính toán phụ thuộc như minh hoạ for-comprehension. Collection hỗ trợ map và flatmap được coi như là một Monadic. Hầu hết các collection trong Scala là Monadic và các xử lý trên chúng sử dụng map và flatMap hoặc sử dụng for-comprehension được coi như là kiểu Monadic. Mọi người thường chỉ xem bản chất Monadic của collection sử dụng từ khoá Monad ví dụ List Monad…

Cho đến nay thì chúng ta đã làm việc với các Functor và Monad trong Collection, nhưng không phải toàn bộ collection là Monad và trong thực tế thì collection là một vài kiểu điển hình ví dụ của Monad. Có rất nhiều Monad là container và wrapper nên nó có thể là ví dụ hũu ích để hiểu Monad không phải là collection.

Option Monad

Một trong những Monad đầu tiên mà chúng ta hay gặp đó là Option monad (được xem như là Maybe Monad trong Haskell và Optional trong Java 8). Bạn có thể coi nó như một Collection mà chứa duy nhất một phần tử. Nên là nó sẽ chỉ chừa một phần từ hoặc không. Do đó nó thường nắm giữ các kết quả tính toàn mà có thể bị lỗi. Nếu kết quả tính toán thành công, giá trị tính toán được đóng gói trong Option (sử dụng kiểu Some), nếu nó lỗi, nó sẽ chứa giá trị của kiểu được yêu cầu, hay đơn giản là giá trị None. Do đó nó cung cấp một giá trị Null rõ ràng hoặc type-safe thay vì đưa ra một Exception hoặc trả về các tham chiếu Null. Chúng ta có thể chuyển đổi Option sử dụng map

val three = Option(3)
// three: Option[Int] = Some(3)
val twelve = three map (_ * 4)
// twelve: Option[Int] = Some(12)

Nhưng khi chúng ta bắt đầu tổng hợp kết quả của nhiều tính toán mà có thể lỗi, chúng ta có thể gặp chính xác vấn đề giống như trước đấy

val four = Option(4)
// four: Option[Int] = Some(4)
val twelveB = three map (i => four map (i * _))
// twelveB: Option[Option[Int]] = Some(Some(12))

Chúng ta nhận được kết quả Option được lồng vào trong Option khác, đây là điều mà chúng ta không mong muốn. Nhưng hiện tại thì chúng ta đã có giải pháp là thay map đầu tiên bằng flatMap hoặc tốt nhất là vẫn sử dụng for-comprehension

val twelveC = three flatMap (i => four map (i * _))
// twelveC: Option[Int] = Some(12)
val twelveD = for {
  i <- three
  j <- four
} yield (i * j)
// twelveD: Option[Int] = Some(12)

Ở đây, một lần nữa chúng ta thấy rằng for-compreshension là dễ hiểu hơn một chút so với việc gọi một chuỗi các map và flatMap lồng nhau. Lưu ý rằng trong For-compreshenstion chúng ta không quan tâm về việc liệu Option có thực sự chứa giá trị hay không – chúng ta chỉ tập trung đến trường hợp thành công, nơi cả 2 thực hiện và an toàn khi hiểu rằng Option Monad sẽ xử lý trường hợp lỗi cho chúng ta.  Cả 2 trường hợp đều có thể lỗi như bên dưới đây

val oops: Option[Int] = None
// oops: Option[Int] = None
val oopsB = for {
  i <- three
  j <- oops
} yield (i * j)
// oopsB: Option[Int] = None
val oopsC = for {
  i <- oops
  j <- four
} yield (i * j)
// oopsC: Option[Int] = None

Đây là lợi ích điển hình khi chúng ta viết code theo kiểu Monadic. Chúng ta kết nối nhiều tính toán với nhau chỉ nghĩ về các trường hợp kinh điển và tin rằng Monad sẽ xử lý bất kỳ trường hợp tính toán thêm vào cho chúng ta.

Future Monad

Chúng ta sẽ cùng tìm hiểu về Monad cho các tính toán chạy song song và bất đồng bộ, Future Monad. Future Monad thường sử dụng để đóng gói các tính toán chậm, và gửi chúng đến các thread khác nhau để hoàn thành. Cuộc gọi trả về Future ngay lập tức, cho phép thread đang được gọi tiếp tục cho đến khi thread thêm vào xử lý các công việc chậm. Ở trạng thái này, Future sẽ không hoàn thành và sẽ không chứa giá trị. Nhưng ở một vài thời điểm mà chúng ta không thể đoán trước được trong Future nó sẽ hoàn thành. Trong đoạn code ví dụ bên dưới, chúng ta sẽ xây dựng 2 Future mà mỗi cái sẽ mất ít nhất 10s để hoàn thành. Trong thread chính, chúng ta sẽ sử dụng for-compresension để nối 2 tính toán lại với nhau.  Một lần nữa, cái này ngay lập tức trả về một Future khác mà tại một thời điểm nào đó trong Future sẽ chứa kết quả của phép tính bắt nguồn. Sau đó, để minh hoạ một cách đơn giản, chúng ta bắt thread chính dừng và đợi Future thứ 3 (f3) hoàn thành và in kết quả ra console.

import scala.concurrent.duration._
import scala.concurrent.{Future,ExecutionContext,Await}
import ExecutionContext.Implicits.global
val f1=Future{
  Thread.sleep(10000)
  1 }
val f2=Future{
  Thread.sleep(10000)
  2 }
val f3=for {
  v1 <- f1
  v2 <- f2
  } yield (v1+v2)
println(Await.result(f3,30.second))

Khi bạn chạy đoạn code trên bạn sẽ thấy rằng kết quả sẽ chạy mất 10s, do f1 và f2 2 chạy song song trên 2 thread khác nhau. Do đó, Future Monad là một cách để bắt đầu với lập trình song song và bất đồng bộ trong Scala

You May Also Like

About the Author: Nguyen Dinh Thuc

Leave a Reply

Your email address will not be published.