Functor, Applicative, Monad bằng tranh vẽ
Mình tìm thấy một bài rất hay về Functional Programming (lập trình hàm), giải thích các khái niệm bằng hình vẽ dễ hiểu. Bài này mình sẽ dịch lại và viết bằng Swift (!) Bắt đầu nhé Đây là một giá trị rất đơn giản Chúng ta đều biết làm thế nào để áp một hàm (function) và giá trị ...
Mình tìm thấy một bài rất hay về Functional Programming (lập trình hàm), giải thích các khái niệm bằng hình vẽ dễ hiểu. Bài này mình sẽ dịch lại và viết bằng Swift (!)
Bắt đầu nhé
Đây là một giá trị rất đơn giản
Chúng ta đều biết làm thế nào để áp một hàm (function) và giá trị
Quá đơn giản! Bây giờ hãy tổng quát hóa một chút, hãy tưởng tượng giá trị bên trên được "gói" vào một cái hộp, giống như là ngữ cảnh của riêng giá trị đó
Bây giờ khi bạn áp một hàm vào cả cục này (hộp bên trong chứa giá trị), bạn có thể nhận được các giá trị khác nhau phụ thuộc vào các ngữ cảnh cụ thể khác nhau. Đây là ý tưởng khởi thủy của Functors, Applicatives và Monad. Ở đây giả sử có 2 ngữ cảnh (2 cái hộp) có thể xảy ra, 1 là hộp chứa giá trị thật và một là hộp không chứa gì cả.
Kiểu dữ liệu này tạm gọi là Maybe. Trong Swift Maybe có thể định nghĩa dễ dàng bằng enum
enum Maybe<T> { case Just(T) case Nothing }
fmap
Khi một giá trị đã được "gói" trong một ngữ cảnh, bạn không thể chỉ đơn giản áp hàm lên nó như trước nữa.
Đây là thời khắc xuất hiện của fmap. fmap rất cool ngầu, fmap là dân chơi!, fmap có "ma thuật" để áp được hàm vào giá trị đang gói trong hộp!
Boom! fmap vừa thể hiện cho chúng ta hắn hổ báo thế nào. Bây giờ chúng ta có thể bắt đầu làm quen với khái niệm Functor.
Functors
Functor là một kiểu dữ liệu mà có thể định nghĩa được (có thể tồn tại) một hàm fmap giống như trên
Như thế nào gọi là "giống như trên"
Nghĩa là fmap có thể nhận vào một hàm và một Functor, sau đó trả lại một Functor khác.
Functor ở đây là ngữ cảnh gói giá trị (chiếc hộp) ở bên trên. Hãy nhìn cụ thể những gì xảy ra khi fmap biểu diễn!
Bạn thấy đấy, fmap đầu tiên nhận vào một tham số là một hàm (hàm +3 trong trường hợp này), rồi nhận tiếp tham số tiếp theo là Funtor(Just 2) và trả lại một Functor mới (Just 5). Let's Swift!
Maybe.Just(2).fmap { i in i+3 } // Maybe.Just(5)
Đây sẽ là những gì xảy ra khi đoạn mã trên được chạy, khi ngữ cảnh là hộp xanh (có giá trị)
và hộp hồng (không có giá trị)
Maybe.Nothing.fmap { i in i+3 } // Maybe.Nothing
Nothing đi vào và Nothing lại đi ra! Bill O'Reilly hoàn toàn không thích điều này!
Giống như thủ lĩnh Morpheus(Matrix), fmap đơn giản là biết cần phải làm gì với Functor. Nếu Functor chứa giá trị thì fmap sẽ trả lại Functor chứa giá trị, nếu Functor không chứa gì cả thì fmap cũng không trả lại gì cả. fmap là Zen!
Hãy tưởng tượng đoạn code sau trong Ruby
let post = Post.findByID(1) if post != nil { return post.title } else { return nil }
Với functor thì chúng ta sẽ viết là
findPost(1).fmap(getPostTitle)
Nếu findPost trả lại 1 Post, chúng ta sẽ lấy title của Post đó, còn nếu findPost trả lại Nothing thì ta cũng trả lại Nothing!
Đây là ví dụ nữa miêu tả khi bạn áp một hàm (+3) vào một list
List cũng là Functor!
Tiếp nhé, bạn có nghĩ chúng ta có thể áp một hàm vào một hàm khác ?
Giả sử đây là hình ảnh một hàm (-1):
Vậy như thế này sẽ gọi là áp hàm vào hàm khác,
Kết quả là 1 hàm ghép của 2 hàm trước đó!
Như vậy bản thân hàm (function) cũng là một dạng Functor!
Applicatives
Applicatives trừu tượng hơn một mức. Bây giờ giá trị của chúng ta đã được gói trong ngữ cảnh
và cả hảm để áp dụng cũng được gói trong ngữ cảnh nốt
Bây giờ cái để áp hàm trong ngữ cảnh vào giá trị trong ngữ cảnh, gọi là Applicatives, ở đây ký hiệu là <*>
Maybe.Just({ i in i + 3 }) <*> Maybe.Just(2) // Maybe.Just(5)
Sử dụng Applicatives này có thể tạo ra các hình huống thú vị khi thao tác với list như thế này
Có một số khả năng giới hạn của Functors không làm được, nhưng lại khả thi với Applicatives: Do Applicatives là "ma thuật" giữa giá trị gói trong ngữ cảnh và hàm gói trong ngữ cảnh, còn Functors chỉ là "ma thuật" giữa giá trị gói trong ngữ cảnh và hàm, nên Applicatives có thể thao tác với các hàm có nhiều tham số cùng với các tham số đó gói trong ngữ cảnh =). Đọc xong xoắn hết cả lưỡi =)
Monad
Để hiểu được Monad là gì bạn cần phải
- Lấy một bằng PhD trong bộ môn khoa học máy tính
- Vứt cái bằng đấy đi vì nó dek cần thiết để hiểu cái mình sắp nói này
Monad là một thể loại xoắn mới.
Functors là kiểu dữ liệu mà có thể áp một hàm vào một giá trị gói trong ngữ cảnh
Applicatives là kiểu dữ liệu mà có thể áp một hàm gói trong ngữ cảnh vào một giá trị gói trong ngữ cảnh
Monad là kiểu dữ liệu mà:
Có thể áp một hàm trả lại một giá trị gói trong ngữ cảnh vào một giá trị gói trong ngữ cảnh.
Hãy nhìn lại Maybe của chúng ta
Giả sử half là hàm chỉ trả lại giá trị khi giá trị là số chẵn. Hàm này trả lại một giá trị gói trong ngữ cảnh Maybe<Int>
func half(a: Int) -> Maybe<Int> { return a % 2 == 0 ? Maybe.Just(a / 2) : Maybe.Nothing }
Nếu cố sống cố chết tống một giá trị đã gói vào hàm này thì sao?
Chúng ta sẽ cần đến Monad, tạm đặt tên là >>= ở đây. Đây là hình dung của Monad trong tưởng tượng =)
Maybe.Just(3) >>= half // Maybe.Nothing Maybe.Just(4) >>= half // Maybe.Just(2) Maybe.Nothing >>= half // Maybe.Nothing
Điều gỉ xảy ra bên trong >>=. Mình sẽ đưa ra signature của monad này nhé
func >>=<T, U>(a: Maybe<T>, f: T -> Maybe<U>) -> Maybe<U>
f ở đây sẽ được truyền vào hàm half sau này, còn bản thân a đã là một giá trị gói trong ngữ cảnh Maybe<T>
Hình dung như sau
Như vậy bản thân Maybe của ta ở thời điểm này đã là một monad! Đây là điều xảy ra khi ném Maybe.Just(3) vào half
Đây là điều xảy ra khi ném Maybe.Nothing vào half
Cái hay ở đây là chúng ta có thể chain liên tiếp
Maybe.Just(20) >>= half >>= half >>= half // Maybe.Nothing
Ở đây điều thực sự xảy ra sẽ là
Ngầu cool ! Giờ chúng ta đã biết Maybe vừa là Functor, vừa là Applicative, vừa là Monad!
IO cũng là một monad!
IO trong Haskell là một monad. Rất tiêc mình chưa tìm được ứng dụng tương ứng trong Swift. Tuy vậy hình vẽ sau đây cũng đủ dễ hiểu
Hàm getLine nhận user input
Hàm readFile nhận một xâu là tên file và cũng trả lại IO monad
Thêm nữa, púttrLn cũng nhận một xâu và in ra màn hình
Vậy giờ chúng ta sẽ chain 3 hàm này. Sở dĩ có thể chain được là vì cả 3 hàm đều trả về giá trị gói trong ngữ cảnh (IO)
getLine >>= readFile >>= putStrLn
Trong Haskell có cách viết ngắn gọn dùng do
foo = do filename <- getLine contents <- readFile filename putStrLn contents
Trong Scala thì đây chính là for yield hay còn gọi là comprehension for, nối nhiều flatMap lại với nhau.
Lời kết
- Một functor là kiểu dữ liệu mà có thể định nghĩa được hàm map
- Một applicative là kiểu dữ liệu mà có thể định nghĩa được hàm apply
- Một monad là kiểu dữ liệu mà có thể định nghĩa được hàm flatMap
- Maybe trong ví dụ trên là Functor, Applicative và Monad. Điều này đúng với cả kiểu Optional của thư viện chuẩn Swift
Điểm khác biệt giữa 3 loại là
- functor áp hàm vào biến gói trong ngữ cảnh
- applicative áp hàm gói trong ngữ cảnh vào biến gói trong ngữ cảnh
- monads áp một hàm trả về một biến trong ngữ cảnh, vào một biến trong ngữ cảnh khác.
Nếu bạn đọc đến đây và đã cảm thấy Monads là một ý tưởng rất thông thái, thì hãy check thử section on Monads. Có rất nhiều món hay ho trong đó mà mình chưa thể cover hết ở đây
Reference
- Functors, Applicatives, And Monads In Pictures
- Code của bài viết này: IBM Swift Sandbox
- Nếu bạn muốn xem ngay implement của Maybe toàn năng bên trên:
enum Maybe<T> { case Just(T) case Nothing func fmap<U>(f: T -> U) -> Maybe<U> { switch self { case .Just(let x): return .Just(f(x)) case .Nothing: return .Nothing } } func apply<U>(f: Maybe<T -> U>) -> Maybe<U> { switch f { case .Just(let JustF): return self.fmap(JustF) case .Nothing: return .Nothing } } func flatMap<U>(f: T -> Maybe<U>) -> Maybe<U> { switch self { case .Just(let x): return (f(x)) case .Nothing: return .Nothing } } } infix operator >>= { associativity left } func >>=<T, U>(a: Maybe<T>, f: T -> Maybe<U>) -> Maybe<U> { return a.flatMap(f) }