07/09/2018, 15:39

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 (!)

Functor, Applicative and Monad

Bắt đầu nhé :smile:

Đâ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ảnhhà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 :smiling_imp:

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: :smile:
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)
}
0