Monad trong Scala
Bài đăng này đã không được cập nhật trong 9 năm
TL/DR
Trong Scala, một kiểu chỉ cần hỗ trợ flatMap
là có thể xem như là một Monad. Lợi ích trước mắt là có thể sử dụng trong for comprehension.
Ví dụ thế này, ta có một đĩa chứa vài quả ổi. Thêm vào đó, ta biết cách bổ từng quả ổi ra rồi bày lên đĩa. Khi đó, nếu cái đĩa ổi kia là một Monad thì ta có thể dùng một cái máy kì lạ có tên là flatMap
. Ta nhét đĩa ổi vào cái máy này, bảo nó cách bổ ổi bày ra đĩa, thì nó sẽ tự biết cách bổ hết các quả ổi có trong đĩa, rồi bày tất cả lên chung một cái đĩa khác.
val đĩa_quả_ổi = Đĩa(Ổi("to"), Ổi("găng"), Ổi("sâu"))
def bổ_ổi(ổi: Ổi): Đĩa[Miếng_Ổi] = {
val các_miếng_ổi = cắt(ổi)
Đĩa(các_miếng_ổi)
}
val đĩa_miếng_ổi = đĩa_quả_ổi.flatMap{ bổ_ổi(_) }
Monad là gì
I. Định nghĩa
Có thể xem một monad là một giá trị được gắn kèm thêm ngữ cảnh. Ví dụ
như Option[T]
thể hiện một giá trị của kiểu T
với ngữ cảnh là
có khả năng lỗi sẽ xảy ra. Hay nếu kết quả trả về của một hàm là
List[T]
thì ta biết hàm đó có thể có 0,
1, hoặc nhiều giá trị trả về.
Theo Wikipedia, monad gồm ba thành phần:
- Type constructor
- Unit function
- Binding operation
1. Type constructor
Type constructor định nghĩa cách tạo một kiểu monadic từ một kiểu cụ thể
nào đó. Kiểu monadic này nhận một tham số kiểu, giống như generic trong
Java hay template của C++ vậy. Ví dụ: Option[T]
, List[T]
chưa phải
là một kiểu cụ thể, nhưng Option[String]
hay Option[Int]
là kiểu cụ
thể, nhận tham số kiểu là String
và Int
tương ứng.
2. Unit function
Unit function là một hàm đính kèm thêm ngữ cảnh vào một giá trị cụ
thể, từ A -> Monad[A]
. Kết quả thu được thường là giá trị "đơn giản
nhất" mà vẫn giữ được thông tin giá trị đầu vào.
Ví dụ đối với Option
, ta có Some(_)
; với List
sẽ là List(_)
3. Binding Operation
Binding operation có kiểu Monad[A] -> (A -> Monad[B]) -> Monad[B]
Tham số đầu tiên là một giá trị có kiểu monadic, tham số thứ hai là một hàm biến đổi từ kiểu mà monad trong tham số đầu tiên đóng gói sang một kiểu monadic khác, kết quả thu được thuộc kiểu monadic kia. Binding operation có thể xem như gồm bốn bước:
- Các giá trị chứa trong monad tham số đầu tiên được lấy ra
- Hàm số trong tham số thứ hai được áp dụng cho tất cả các giá trị vừa thu được, kết quả thu được cũng là các monad
- Lấy các giá trị từ các monad vừa tính toán được ra
- Từ các giá trị vừa lấy ra được, tạo thành một giá trị monad duy nhất
Binding operation trong Scala thông thường có tên gọi là flatMap
.
Ngoài ra, monad còn phải tuân thủ các luật monad.
II. Các luật Monad
1. Left identify
Luật monad đầu tiên yêu cầu rằng nếu ta có một giá trị, thêm ngữ cảnh
với unit function
rồi truyền nó vào binding operation
cùng một hàm
thì sẽ tương đương với việc truyền trực tiếp giá trị ban đầu vào hàm đó.
Ví dụ với Option
thì Some(x).flatMap(f)
chính bằng f(x)
, hay với
List
thì List(x).flatMap(f)
thì không khác gì f(x)
.
scala> def f(x: Int) = List(x, -x)
f: (x: Int)List[Int]
scala> List(1).flatMap(f)
res14: List[Int] = List(1, -1)
scala> f(1)
res15: List[Int] = List(1, -1)
2. Right identity
Luật thứ hai yêu cầu rằng, nếu ta có một giá trị monadic, truyền vào
binding operation
cùng với tham số thứ hai là unit function
thì kết
quả thu được phải là monad ban đầu.
Cụ thể với Option
thì x.flatMap(Some(_))
phải trả về x
, cũng
như đối với List
thì x.flatMap(List(_))
cũng như x
scala> Some(10).flatMap(Some(_))
res19: Option[Int] = Some(10)
scala> None.flatMap(Some(_))
res20: Option[Nothing] = None
scala> List('a','b','c').flatMap(List(_))
res21: List[Char] = List(a, b, c)
scala> List().flatMap(List(_))
res22: List[Nothing] = List()
3. Associativity (kết hợp)
Luật monad cuối cùng là khi ta 'chain' các binding operation với nhau thì kết quả không phụ thuộc vào cách ta đặt dấu ngoặc: binding hai hàm liên tiếp giống như binding vào một hàm số có thể xác định từ hai hàm ban đầu.
Một cách hình thức x.flatMap(f).flatMap(g)
tương đương với
x.flatMap(f(_).flatMap(g))
Ví dụ:
scala> def f(x:Int) = List(x, -x)
f: (x: Int)List[Int]
scala> def g(x:Int) = List(2*x)
g: (x: Int)List[Int]
scala> List(1,2).flatMap(f).flatMap(g)
res24: List[Int] = List(2, -2, 4, -4)
scala> List(1,2).flatMap(f(_).flatMap(g))
res25: List[Int] = List(2, -2, 4, -4)
III. Scala for comprehensions
Trong scala, for comprehensions được biến đổi sử dụng flatMap
Ví dụ:
for(
val x <- m1
val y <- m2
) yield x+y
Tương đương với
m1.flatMap(x => m2.flatMap(y => List(x+y)))
Tuy nhiên, flatMap cuối cùng hơi thừa, nên thực tế Scala biến đổi thành:
m1.flatMap(x => m2.map(y => x+y))
Ví dụ áp dụng Monad Option
trong Scala
Trong phần này, ta sẽ xét một ví dụ để thấy được tác dụng của Monad
Option
cũng như binding operation flatMap
.
Giả sử ta có Person
như sau:
object Person {
val persons = List("P", "MP", "MMP", "FMP", "FP", "MFP", "FFP") map { Person(_) }
private val mothers = Map(
Person("P") -> Person("MP"),
Person("MP") -> Person("MMP"),
Person("FP") -> Person("MFP"))
private val fathers = Map(
Person("P") -> Person("FP"),
Person("MP") -> Person("FMP"),
Person("FP") -> Person("FFP"))
def mother(p: Person): Option[Person] = mothers.get(p)
def father(p: Person): Option[Person] = fathers.get(p)
}
case class Person(name: String) {
def mother: Option[Person] = Person.mother(this)
def father: Option[Person] = Person.father(this)
}
Như vậy, ta có một danh sách các người ["P", "MP", "MMP", "FMP", "FP",
"MFP", "FFP"], với mỗi người ta có thể xem thông tin về cha hoặc mẹ của
người đó: nếu có sẽ trả về Some(person)
còn không sẽ trả về None
Giờ nếu ta muốn tìm ông ngoại của một người, ta có thể viết:
def maternalGrandfatherNoMonad(p: Person): Option[Person] =
p.mother match {
case Some(m) => m.father
case None => None
}
Còn nếu ta muốn kiểm tra xem cả ông nội và ông ngoại của một người có nằm trong cơ sở dữ liệu không thì:
def bothGrandfathersNoMonad(p: Person): Option[(Person, Person)] =
p.father match {
case None => None
case Some(f) =>
f.father match {
case None => None
case Some(ff) =>
p.mother match {
case None => None
case Some(m) =>
m.father match {
case None => None
case Some(fm) => Some(ff, fm)
}
}
}
}
Với mỗi truy vấn tìm cha hoặc mẹ của một người mà lỗi, trả về None
thì
tất cả hàm của ta cũng phải trả về None
. Khi không có monad, ta phải
dùng pattern matching nhiều lần, xét tất cả các trường hợp có thể xảy
ra, hàm thu được vừa dài, vừa khó đọc.
May mắn là chúng ta có monad. Khi dùng monad, hai hàm của ta có thể được đơn giản hoá thành:
def maternalGrandfather(p: Person) : Option[Person] =
p.mother.flatMap { _.father }
def bothGrandfathers(p: Person) : Option[(Person, Person)] =
p.father.flatMap(f =>
f.father.flatMap(ff =>
p.mother.flatMap(m =>
m.father.flatMap(fm =>
Some(ff, fm)
)
)
)
)
Nếu dùng for comprehensions thì bothGrandfathers
còn dễ đọc hơn
def bothGrandfathersFor(p: Person): Option[(Person, Person)] =
for (
f <- p.father;
ff <- f.father;
m <- p.mother;
fm <- m.father
) yield (ff, fm)
Tài liệu tham khảo
All rights reserved