Chương 12: Một số vấn đề về Monad

Trở về Mục lục cuốn sách

Khi lần đầu tiên nói về functor, chúng ta đã thấy rằng đó là khái niệm hữu ích cho những giá trị mà ta có thể ánh xạ lên chúng. Sau đó, ta đã phát triển thêm khái niệm này bằng cách giới thiệu các functor áp dụng, vốn cho phép ta xem các giá trị thuộc kiểu dữ liệu nhất định như những giá trị gắn với ngữ cảnh và dùng các hàm thông thường lên các giá trị đó trong khi vẫn giữ được ý nghĩa của các ngữ cảnh đó.

Trong chương này, ta sẽ làm quen với monad, vốn là các functor ứng dụng được nâng cao, cũng giống như bản thân các functor ứng dụng là các functor nâng cao.

more cool than u

Khi ta bắt đầu làm quen với functor, ta đã thấy rằng có thể ánh xạ các hàm lên nhiều kiểu dữ liệu khác nhau. Ta thấy được rằng, để phục vụ mục đích này, lớp Functor đã được giới thiệu và chúng khiến ta đặt câu hỏi: khi ta có một hàm thuộc kiểu a -> b và một kiểu dữ liệu nào đó, f a, thì làm thế nào để ta ánh xạ hàm này lên kiểu dữ liệu nêu trên để thu được f b? Ta đã thấy cách ánh xạ một thứ gì đó lên một Maybe a, một danh sách [a], một IO a v.v. Thậm chí ta còn thấy cách ánh xạ một hàm a -> b lên các hàm khác thuộc kiểu r -> a để thu được hàm thuộc kiểu r -> b. Muốn trả lời câu hỏi làm thế nào để ánh xạ hàm lên một kiểu dữ liệu nào đó, thì tất cả những gì ta phải làm là nhìn vào kiểu của fmap:

fmap :: (Functor f) => (a -> b) -> f a -> f b

Rồi sau đó làm cho nó hoạt động được với kiểu dữ liệu đang xét bằng cách viết thực thể Functor phù hợp.

Khi ta thấy một cách cải thiện có thể được đối với functor và nói: Này, sẽ thế nào nếu hàm a -> b đó đã được bọc sẵn trong một giá trị functor? Chẳng hạn, nếu ta có Just (*3), làm thế nào ta có thể áp dụng thứ trên cho Just 5? Ta sẽ làm gì nếu không muốn áp dụng nó cho Just 5 nhưng lại muốn áp dụng cho Nothing ? Hoặc nếu có [(*2),(+4)], làm thế nào để áp dụng nó cho [1,2,3]? Thậm chí, bằng cách nào mà việc áp dụng này hoạt động được? Để trả lời những câu hỏi trên, lớp Applicative được giới thiệu, trong đó ta muốn lời giải đáp cho kiểu dữ liệu như sau:

(<*>) :: (Applicative f) => f (a -> b) -> f a -> f b

Ta cũng có thể thấy rằng ta có thể lấy một giá trị thông thường để gói nó vào trong một kiểu dữ liệu. Chẳng hạn, ta có thể lấy một giá trị 1 và gói lại để nó trở thành Just 1. Hoặc để nó trở thành [1]. Hoặc trở thành một thao tác I/O chẳng để thực hiện điều gì mà chỉ trả lại 1. Hàm thực hiện điều này được gọi là thuần túy.

Như ta đã nói, một giá trị áp dụng được có thể coi như một giá trị kèm theo một ngữ cảnh. Một giá trị fancy, nếu nói theo thuật ngữ tiếng Anh. Chẳng hạn, kí tự 'a' chỉ là một kí tự thông thường, nhưng Just 'a' lại có thêm một ngữ cảnh. Ở đây thay vì Char, ta có một Maybe Char, để cho ta biết rằng giá trị này có thể là một kí tự, nhưng cũng có thể là một sự vắng mặt của kí tự.

Ta thấy được nét gọn gàng trong việc lớp Applicative cho phép dùng các hàm thông thường đối với các giá trị kèm ngữ cảnh nêu trên và cách mà ngữ cảnh đó được duy trì. Hãy xem nhé:

ghci> (*) <$> Just 2 <*> Just 8
Just 16
ghci> (++) <$> Just "klingon" <*> Nothing
Nothing
ghci> (-) <$> [3,4] <*> [1,2,3]
[2,1,0,3,2,1]

À, hay đấy, bây giờ ta có thể coi chúng như các giá trị áp dụng; các giá trị Maybe a biểu diễn cho những đại lượng tính toán có thể bị thất bại, các giá trị [a] biểu diễn cho những đại lượng có thể nhận nhiều kết quả (đại lượng không tất định), các giá trị IO a biểu diễn những giá trị có hiệu ứng phụ, v.v.

Monad là một sự mở rộng tự nhiên đối với các functor áp dụng; với monad, ta quan tâm đến điều này: nếu bạn có một giá trị kèm theo ngữ cảnh, m a, thì làm thế nào để áp dụng nó cho một hàm vốn nhận một a thông thường và trả lại một giá trị kèm theo ngữ cảnh? Tức là, làm thế nào để áp dụng một hàm có kiểu a -> m b cho một giá trị có kiểu m a? Như vậy, nhất định là ta sẽ muốn có hàm sau đây:

(>>=) :: (Monad m) => m a -> (a -> m b) -> m b

Nếu có một giá trị fancy và một hàm nhận một giá trị thông thường nhưng trả lại một giá trị fancy, thì làm thế nào để đựa giá trị fancy này vào hàm nêu trên? Đây là câu hỏi chính mà ta sẽ quan tâm đến khi làm việc với monad. Ta viết m a thay vì f am thay thế cho Monad, nhưng monad chỉ là các các functor áp dụng có hỗ trợ >>=. Hàm >>= được đọc là gắn.

Khi ta có một giá trị thông thường a và một hàm thông thường a -> b sẽ dễ dàng đưa giá trị này vào trong hàm — bạn chỉ việc áp dụng hàm lên giá trị như thông thường và thế là xong. Nhưng khi ta xử lý các giá trị kèm theo ngữ cảnh nhất định, sẽ cần suy nghĩ một chút để xem là gắn các giá trị fancy này vào các hàm như thế nào và bằng cách nào phân tích được hành vi của chúng, nhưng bạn sẽ thấy rằng nó sẽ dễ như việc đếm đến ba:

Bắt tay vào xử lý Maybe

monads, grasshoppa

Bây giờ khi đã hình dung sơ qua về monad, ta hãy xem liệu có thể làm rõ khái niệm này không.

Chẳng có gì ngạc nhiên khi biết rằng Maybe là một monad, vậy ta hãy khám phá thêm về nó và xem rằng liệu ta có thể kết hợp nó với những điều ta biết về monad:

Lúc này, hãy chắc chắn là bạn đã hiểu các applicative. Sẽ rất tốt nếu bạn có cảm nhận được cách hoạt động của các thực thể Applicative khác nhau và việc chúng biểu diễn cho các đại lượng nào, vì monad không có gì hơn là việc lấy kiến thức mà ta có sẵn về applicative rồi nâng cao lên.

Một giá trị thuộc kiểu Maybe a biểu diễn cho một giá trị kiểu a kèm theo ngữ cảnh là việc tính toán có thể bị thất bại. Một giá trị Just "dharma" có nghĩa là chuỗi "dharma" có ở đó, trong khi một giá trị Nothing biểu diễn sự vắng mặt của chuỗi này, hoặc nếu bạn coi chuỗi như là kết quả tính toán, thì điều đó nghĩa là việc tính toán đã thất bại.

Khi ta xem Maybe như là một functor, ta thấy được rằng nếu muốn fmap một hàm lên nó, thì functor này sẽ bị ánh xạ tất cả các phần tử trong nó, nếu nó là một giá trị Just, còn nếu không thì Nothing được giữ lại vì khi đó không có gì để ánh xạ lên nữa!

Như thế này:

ghci> fmap (++"!") (Just "wisdom")
Just "wisdom!"
ghci> fmap (++"!") Nothing
Nothing

Giống một functor áp dụng, nó hoạt động tương tự như vậy. Tuy nhiên các applicatives cũng có hàm được gói lại. Maybe là một functor áp dụng sao cho khi ta dùng <*> để áp dụng một hàm ở trong một Maybe lên một giá trị vốn nằm trong một Maybe, thì chúng phải cùng là các giá trị Just nếu muốn có kết quả là giá trị Just, còn nếu không thì kết quả sẽ là Nothing. Điều này có lý vì nếu bạn viết thiếu một trong hai thứ: hàm hoặc thứ mà bạn áp dụng hàm lên, thì bạn sẽ không thể gột nên hồ khi không có bột, vì vậy bạn sẽ làm lan truyền thông tin về việc phép tính bị thất bại:

ghci> Just (+3) <*> Just 3
Just 6
ghci> Nothing <*> Just "greed"
Nothing
ghci> Just ord <*> Nothing
Nothing

Cũng tương tự khi dùng phong cách áp dụng để khiến các hàm thông thường hoạt động với giá trị Maybe. Tất cả những giá trị đều phải là giá trị Just, nếu không thì tất cả sẽ là Nothing!

ghci> max <$> Just 3 <*> Just 6
Just 6
ghci> max <$> Just 3 <*> Nothing
Nothing

Và bây giờ, hãy nghĩ về cách làm thế nào để viết >>= cho Maybe. Như đã nói, >>= nhận một giá trị monad, và một hàm nhận vào giá trị thông thường và trả lại một giá trị monad rồi cố gắng áp dụng hàm đó cho giá trị monad. Làm thế nào để nó hoạt động, nếu hàm chỉ nhận giá trị thường. Ồ, để làm được điều này, nó phải tính đến ngữ cảnh của giá trị monad đó.

Trong trường hợp này, >>= sẽ nhận một giá trị Maybe a và một hàm thuộc kiểu a -> Maybe b rồi bằng cách nào đó áp dụng hàm lên Maybe a. Để hifnnh dung ra cách hoạt động, ta có thể dùng trực giác đã được phát huy từ trường hợp của Maybe vốn cũng là functor áp dụng. Chẳng hạn, giả sử ta có một hàm \x -> Just (x+1). Nó nhận vào một số, cộng với 1 rồi gói kết quả vào trong một Just:

ghci> (\x -> Just (x+1)) 1
Just 2
ghci> (\x -> Just (x+1)) 100
Just 101

Nếu ta đưa 1 vào, nó sẽ lượng giá thành Just 2. Nếu ta đưa vào số 100, kết quả sẽ là Just 101. Rất dễ thấy. Bây giờ mới là chỗ khó: làm sao để đưa một giá trị Maybe vào trong hàm này? Nếu ta hình dung Maybe đóng vai trò một functor áp dụng, thì sẽ dễ dàng trả lời câu hỏi nêu trên. Nếu ta đưa vào một giá trị Just thì nó lấy thứ ở bên trong Just rồi áp dụng hàm lên thứ đó. Nếu ta đưa vào một Nothing, thì, hừm, ta sẽ chỉ còn lại một hàm áp dụng lên Nothing. Và trong trường hợp này thì nó sẽ chỉ làm những gì trước đây ta làm và báo kết quả là Nothing.

Thay vì gọi nó là >>=, từ giờ ta hãy gọi nó là applyMaybe. Nó nhận một Maybe a và một hàm vốn trả về một Maybe b rồi cố gắng áp dụng hàm này lên Maybe a. Sau đây là mã lệnh thực hiện:

applyMaybe :: Maybe a -> (a -> Maybe b) -> Maybe b
applyMaybe Nothing f  = Nothing
applyMaybe (Just x) f = f x

Được rồi, bây giờ ta hãy nghịch chơi một lát. Ta sẽ dùng nó như hàm trung tố để cho giá trị Maybe nằm bên trái và hàm nằm bên phải:

ghci> Just 3 `applyMaybe` \x -> Just (x+1)
Just 4
ghci> Just "smile" `applyMaybe` \x -> Just (x ++ " :)")
Just "smile :)"
ghci> Nothing `applyMaybe` \x -> Just (x+1)
Nothing
ghci> Nothing `applyMaybe` \x -> Just (x ++ " :)")
Nothing

Ở ví dụ trên, ta thấy rằng khi dùng applyMaybe với một giá trị Just và một hàm thì đơn giản là hàm sẽ được áp dụng cho giá trị bên trong Just. Khi ta cố gắng dùng nó với một Nothing, toàn bộ kết quả là Nothing. Sẽ ra sao nếu hàm này trả lại một Nothing? Hãy xem nhé:

ghci> Just 3 `applyMaybe` \x -> if x > 2 then Just x else Nothing
Just 3
ghci> Just 1 `applyMaybe` \x -> if x > 2 then Just x else Nothing
Nothing

Đúng như ta dự liệu. Nếu giá trị monad ở bên trái là một Nothing, thì cả biểu thức lớn là Nothing. Và nếu hàm bên phải trả lại một Nothing, thì kết quả lại là Nothing. Điều này rất giống với khi ta dùng Maybe dưới dạng áp dụng và thu về kết quả Nothing nếu đâu đó ở bên trong xuất hiện một Nothing.

Có vẻ như với Maybe, ta đã hình dung ra cách lấy một giá trị fancy rồi đưa nó vào hàm nhận giá trị thường để hàm trả lại một giá trị fancy. Ta đã làm điều này bằng cách ghi nhớ rằng một giá trị Maybe biểu diễn cho một đại lượng có thể bị thất bại trong quá trình tính toán.

Có thể bạn đang tự hỏi, thế điều này có ích gì? Dường như các functor áp dụng thì mạnh hơn monad, vì functor áp dụng cho phép ta lấy một hàm thường rồi khiến nó hoạt động trên các giá trị kèm ngữ cảnh. Ta sẽ thấy rằng các monad cũng có thể làm điều này vì chúng là dạng nâng cấp của các functor áp dụng, và chúng còn có thể làm những thứ hay ho mà functor áp dụng không thể.

Ta sẽ nhanh chóng trở lại Maybe, nhưng trước tiên, hãy kiểm tra lớp thuộc về các monad.

Lớp Monad

Cũng giống như việc functor có lớp Functor và functor áp dụng có lớp Applicative, monad có lớp riêng của chúng: Monad! Oa, ai mà lường được? Đây là những thứ mà ta thấy ở lớp này:

class Monad m where
    return :: a -> m a

    (>>=) :: m a -> (a -> m b) -> m b

    (>>) :: m a -> m b -> m b
    x >> y = x >>= \_ -> y

    fail :: String -> m a
    fail msg = error msg

this is you on monads

Hãy bắt đầu từ dòng lệnh thứ nhất. Nó nói rằng class Monad m where. Nhưng đợi đã, chẳng phải ta đã nói rằng monad chỉ là các functor áp dụng được tăng cường thêm ư? Không lẽ lại có ràng buộc về lớp ở đây giữa những dòng class (Applicative m) = > Monad m where để cho một kiểu dữ liệu buộc phải là functor áp dụng trước khi muốn là monad? À, nên có, nhưng khi Haskell được tạo lập, mọi người thấy các functor áp dụng không hợp với Haskell vì vậy chúng đã không có mặt ở đó. Nhưng yên tâm đi, mỗi monad là một functor áp dụng, ngay cả khi lời khai báo lớp Monad không phát biểu như vậy.

Hàm đầu tiên mà lớp Monad định nghĩa là return. Nó giống như là pure, chỉ khác ở tên gọi. Kiểu của nó là (Monad m) => a -> m a. Nó nhận một giá trị rồi đặt vào trong ngữ cảnh tối thiểu mặc định miễn là đủ chứa giá trị đó. Nói cách khác, nó nhận vào một thứ gì đó rồi bọc nó trong một monad. Nó luôn làm điều giống như hàm pure thuộc lớp Applicative thực hiện; điều đó nghĩa là ta đã làm quen với return. Ta đã dùng return khi lập trình với I/O. Ta đã dùng nó để lấy một giá trị rồi tạo ra một thao tác I/O giả, không làm gì ngoài việc trả lại giá trị đó. Với Maybe, nó nhận một giá trị rồi bọc vào trong một Just.

Một lưu ý nhỏ:: return không hề giống các lệnh return trong đa số các ngôn ngữ khác. Nó chẳng kết thúc việc thực thi của hàm hay gì đó, mà đơn giản là chỉ nhận một giá trị thường rồi đặt nó vào trong một ngữ cảnh.

hmmm yaes

Hàm tiếp theo là >>=, hoặc bind (gắn). Nó giống như việc áp dụng hàm, nhưng thay vì việc lấy một giá trị thường rồi đưa vào hàm thông thường, nó lấy một giá trị monad (tức là giá trị với một ngữ cảnh) rồi đưa nó vào hàm nhận một giá trị thường nhưng trả lại một giá trị monad.

Tiếp theo, ta có >>. Giờ thì ta không chú ý
nhiều về nó vì nó có một cách tạo lập mặc định và ta gần như không bao giờ phải tự tạo lập khi tạo nên các thực thể Monad .

Hàm cuối cùng của lớp Monadfail. Ta không bao giờ dùng hẳn nó khi viết mã lệnh. Thay vì vậy, Haskell sẽ dùng nó để cho phép việc tính toán thất bại trong một cấu trúc cú pháp đặc biệt đối với monad mà sau này ta sẽ gặp. Bây giờ thì ta không cần phải lo lắng quá nhiều về fail.

Giờ khi đã biết lớp Monad trông ra sao, ta hãy xem làm thế nào mà Maybe là thực thể của Monad!

instance Monad Maybe where
    return x = Just x
    Nothing >>= f = Nothing
    Just x >>= f  = f x
    fail _ = Nothing

return cũng giống như pure, vì vậy cái đó không đòi hỏi suy nghĩa nhiều. Ta chỉ cần làm những gì giống như đã làm với lớp Applicative rồi bọc nó lại trong một Just.

Hàm >>= cũng giống như applyMaybe. Khi đưa Maybe a vào hàm này, ta cần để ý ngữ cảnh và trả lại Nothing nếu giá trị vế trái là Nothing vì nếu không có giá trị ở đây thì sẽ không thể nào áp dụng hàm được. Nếu giá trị là Just thì ta lấy thứ bên trong nó rồi đem áp dụng cho f.

Ta có thể nghịch chơi với Maybe như thể một monad:

ghci> return "WHAT" :: Maybe String
Just "WHAT"
ghci> Just 9 >>= \x -> return (x*10)
Just 90
ghci> Nothing >>= \x -> return (x*10)
Nothing

Không có gì mới và hay ho ở dòng lệnh đầu tiên vì ta đã dùng pure với Maybe rồi và biết được rằng return chỉ là pure dưới một tên gọi khác. Hai dòng tiếp theo cho thấy thêm phần nào về >>= .

Lưu ý cách ta đưa Just 9 vào cho hàm \x -> return (x*10), thì x lấy giá trị 9 bên trong hàm. Dường như ta đã có thể kết xuất được giá trị từ một Maybe mà ta không phải khớp mẫu. Và ta vẫn không mất ngữ cảnh của giá trị Maybe ở đây, vì khi nó là Nothing, thì kết quả của việc dùng >>= cũng sẽ là Nothing.

Thăng bằng trên dây

pierre

Bây giờ khi đã biết cách đưa một giá trị Maybe a vào một hàm có kiểu a -> Maybe b trong khi vẫn tính đến ngữ cảnh của việc tính toán thất bại có thể xảy ra thì hãy xem cách mà ta có thể dùng >>= lặp đi lặp lại để xử lý nhiều giá trị Maybe a cùng lúc.

Pierre quyết định nghỉ việc tại một trại cá và thử việc đi trên dây. Anh ấy không hề kém, nhưng chỉ gặp một vấn đề: những con chim luôn đậu vào cây sào dùng để giữ thăng bằng. Chúng đậu vào một lúc để nghỉ ngơi, chuyện trò và rồi lại tung cánh để kiếm tìm những mẩu vụn bánh mì. Điều này cũng sẽ chẳng làm Pierre bận tâm nếu như số chim đậu bên trái cây sào luôn bằng đúng với số chim đậu bên phải. nhưng đôi khi tất cả lũ chim quyết định đậu vào một bên và rồi làm anh ta mất thăng bằng và ngã nhào (xuống lưới bảo vệ).

Giả sử rằng anh đã giữ thăng bằng nhờ việc số chim đậu bên trái sào bằng không hơn kém so với bên phải là ba chú chim. Như vậy nếu có một con chim bên phải và bốn ở bên trái, thì anh ấy vẫn ổn. Nhưng nếu con chim thứ năm đậu vào bên trái, thì anh sẽ mất thăng bằng và ngã.

Chúng ta sẽ mô phỏng việc chim đậu xuống và bay khỏi sào rồi xem liệu rằng Pierre có giữ được thăng bằng không sau khi một số chim nhất định bay đến rồi bay đi. Chẳng hạn, ta sẽ xem rằng điều gì xảy đến với Pierre khi con chim thứ nhất bay đên sđậu vào bên trái, rồi bốn con chim đến đậu vào bên phải sau đó con chim đã đậu vào bên trái quyết định bay đi.

Ta có thể biểu diễn cây sào chỉ bằng một cặp số nguyên. Số thứ nhất để chỉ số chim đậu bên trái còn số bên phải để chỉ số chim bên phải:

type Birds = Int
type Pole = (Birds,Birds)

Trước hết ta lập một kiểu tuowgn đồng với Int, gọi là Birds, vì ta sẽ dùng số nguyên để biểu diễn số chim ở đó. Tiếp theo ta lập một kiểu tương đồng (Birds,Birds) và gọi là Pole (đừng nhầm với Polish là người Ba Lan).

Tiếp theo, ta sẽ tạo các hàm nhận lấy số chim và đặt chúng lên từng phía của cây sào. Sau đây là các hàm này:

landLeft :: Birds -> Pole -> Pole
landLeft n (left,right) = (left + n,right)

landRight :: Birds -> Pole -> Pole
landRight n (left,right) = (left,right + n)

Khá là thuận lợi. Ta hãy thử dùng các hàm này xem:

ghci> landLeft 2 (0,0)
(2,0)
ghci> landRight 1 (1,2)
(1,3)
ghci> landRight (-1) (1,2)
(1,1)

Để xua chim bay đi ta chỉ cần lấy một số âm làm số chim đậu lên một bên sào. Vì việc chim đậu trên Pole sẽ trả lại một Pole, nên ta có thể xâu chuỗi những lượt áp dụng landLeftlandRight:

ghci> landLeft 2 (landRight 1 (landLeft 1 (0,0)))
(3,1)

Khi ta áp dụng hàm landLeft 1 cho (0,0) ta thu được (1,0). Tiếp theo, ta cho một con chim đậu lên phía phải, kết quả là (1,1). Sau cùng có hai con chim đậu lên phía trái, cho kết quả là (3,1). Ta áp dụng một hàm cho thứ gì đó bằng cách trước hết là viết hàm tiếp theo đó là tham số, nhưng ở đây tốt hơn là Pole đi trước rồi mới hàm biểu diễn chim đậu theo sau. Nếu ta lập một hàm như sau:

x -: f = f x

thì ta có thể áp dụng các hàm bằng cách trước hết là các tham số rồi mới đến hàm:

ghci> 100 -: (*3)
300
ghci> True -: not
False
ghci> (0,0) -: landLeft 2
(2,0)

Bằng việc này, ta có thể lặp lại việc cho chim đậu lên hai đầu sào theo cách dễ đọc hơn:

ghci> (0,0) -: landLeft 1 -: landRight 1 -: landLeft 2
(3,1)

Khá hay! Ví dụ này tương ứng với cái trước đây mà ta liên tiếp cho chim đậu lên sào, chỉ khác là bây giờ mã lệnh đã gọn gàng hơn. Ở đây, sự rõ ràng đã thể hiện rõ khi ta bắt đầu với (0,0) rồi cho một con chim đậu lên đầu trái, một con ở đầu phải, rồi sau cùng là hai con ở đầu trái.

Đến giờ thì vẫn ổn, nhưng điều gì sẽ xảy ra nếu 10 con chim đậu lên một đầu?

ghci> landLeft 10 (0,3)
(10,3)

10 con chim đậu lên đầu trái trong khi chỉ có 3 con ở đầu phải? Chắc chắn như vậy sẽ làm anh Pierre tội nghiệp ngã nhào! Điều này dễ thấy rồi, nhưng điều gì sẽ xảy ra nếu ta có chuỗi chim đậu như sau:

ghci> (0,0) -: landLeft 1 -: landRight 4 -: landLeft (-1) -: landRight (-2)
(0,2)

Dường như mọi việc đều ổn nhưng nếu bạn theo các bước ở đây, bạn sẽ thấy rằng một lúc nào đó sẽ có 4 con chim ở đầu phải và không có con chim nào ở đầu trái! Để sửa điều này, ta phải xem thêm các hàm landLeftlandRight. Từ những gì ta thấy, ta muốn các hàm này có khả năng thất bại trong quá trình hoạt động. Nghĩa là, ta muốn chúng trả lại một giá trị Pole mới nếu sào vẫn thăng bằng, nhưng thất bại nếu số chim đậu mất cân đối rõ rệt. Và để thêm một ngữ cảnh thất bại vào cho giá trị thì không có gì hay hơn là dùng Maybe! Hãy viết lại các hàm này:

landLeft :: Birds -> Pole -> Maybe Pole
landLeft n (left,right)
    | abs ((left + n) - right) < 4 = Just (left + n, right)
    | otherwise                    = Nothing

landRight :: Birds -> Pole -> Maybe Pole
landRight n (left,right)
    | abs (left - (right + n)) < 4 = Just (left, right + n)
    | otherwise                    = Nothing

Thay vì trả lại một Pole, các hàm này trả lại một Maybe Pole. Chúng vẫn nhận số chim và giá trị sào cũ như trước, nhưng lần này thì kiểm tra xem việc cho từng ấy chim đậu lên sào liệu có làm Pierre mất thăng bằng không. Ta dùng chốt canh để kiểm tra xem hiệu số giữa hai số chim đậu lên hai đầu sào có dưới 4 hay không. Nếu đúng, thì ta bọc giá trị Pole mới này vào trong Just rồi trả lại nó. Còn nếu không, ta trả lại Nothing, để chỉ sự thất bại.

Ta hãy cho những sản phẩm này hoạt động xem sao:

ghci> landLeft 2 (0,0)
Just (2,0)
ghci> landLeft 10 (0,3)
Nothing

Đẹp đấy! Khi ta cho chim đậu mà vẫn đảm bảo rằng Pierre vẫn thăng bằng, thì ta có một giá trị Pole mới bọc trong Just. Nhưng khi có nhiều chim đậu lên một đầu sào, ta thu đượcNothing. Điều này hay, nhưng dường như ta mất khả năng cho chim đậu nhiều lần lên sào. Ta không viết được landLeft 1 (landRight 1 (0,0)) nữa vì khi áp dụng landRight 1 lên (0,0), ta khong thu về được Pole, mà là một Maybe Pole. landLeft 1 nhận một Pole và không phải Maybe Pole.

Ta cần một cách để nhận Maybe Pole và đưa nó vào một hàm nhận Pole rồi trả lại một Maybe Pole. Thật may là ta có >>=, để chuyên làm việc này cho Maybe. Hãy thử dùng nó xem sao::

ghci> landRight 1 (0,0) >>= landLeft 2
Just (2,1)

Cần nhớ rằng, landLeft 2 có kiểu là Pole -> Maybe Pole. Ta không thể đơn thuần đưa vào Maybe Pole vốn là kết quả của landRight 1 (0,0), vì vậy ta đã dùng >>= để lấy giá trị đó cùng với ngữ cảnh và đưa nó cho landLeft 2. >>= cho phép ta coi giá trị Maybe như là một giá trị kèm ngữ cảnh vì nếu ta đưa Nothing vào landLeft 2, thì kết quả là Nothing và sự thất bại sẽ lan truyền.

ghci> Nothing >>= landLeft 2
Nothing

Với cách này, bây giờ ta có thể xâu chuỗi các hàm khiến chim đậu (mà bản thân các hàm có thể thất bại trong khi tính toán) vì >>= cho phép ta đưa một giá trị monad vào một hàm nhận giá trị thường.

Sau đây là chuỗi các hàm mô tả chim đậu:

ghci> return (0,0) >>= landRight 2 >>= landLeft 2 >>= landRight 2
Just (2,4)

Ngay ở đầu, ta đã dùng return để nhận một giá trị Pole và gói nó vào trong một Just. Ta đã có thể chỉ cần áp dụng landRight 2 cho (0,0), để thu được kết quả tương tự, nhưng cách lalfm này có thể sẽ thống nhất hơn khi dùng >>= cho từng hàm. Just (0,0) được đưa vào cho landRight 2, tạo ra kêt quả Just (0,2), và đến lượt nó, lại được đưa vào landLeft 2, tạo nên Just (2,2), và cứ như vậy.

Hãy nhớ lại ví dụ kiểu này từ trước khi ta đưa việc thất bại trong tính toán vào:

ghci> (0,0) -: landLeft 1 -: landRight 4 -: landLeft (-1) -: landRight (-2)
(0,2)

Nó không mô phỏng đúng lắm tương tác giữa người đi trên dây và lũ chim vì đến giữa chừng thì anh ta mất thăng bằng nhưng kết quả lại không phản ánh điều đó. Nhưng hãy bắt đầu bằng việc dùng cách áp dụng monad (>>=) thay vì áp dụng thông thường:

ghci> return (0,0) >>= landLeft 1 >>= landRight 4 >>= landLeft (-1) >>= landRight (-2)
Nothing

iama banana

Thật tuyết. Kết quả cuối biểu thị sự thất bại tính toán, đó là điều ta dự định. Hãy xem làm thế nào để có kết quả này. Trước hết, return đặt (0,0) vào một ngữ cảnh mặc định, biến nó thành Just (0,0). Sau đó, Just (0,0) >>= landLeft 1 xảy ra. Vì Just (0,0) là một giá trị Just, nên landLeft 1 được áp dụng cho (0,0), kết quả là Just (1,0), vì các con chim đậu vẫn tương đối thăng bằng. Tiếp theo, Just (1,0) >>= landRight 4 xảy ra và kết quả là Just (1,4) và tính cân bằng vẫn được duy trì, dù rất mong manh. Just (1,4) được đưa vào landLeft (-1). Điều này nghĩa là landLeft (-1) (1,4) xảy ra. Bây giờ vì cách hoạt động của landLeft, mà kết quả là Nothing, vì kết quả giá trị cây sào là mất cân đối. Bây giờ khi đã có trong tay Nothing, ta đưa nó vào landRight (-2), nhưng vì nó là một Nothing, nên kết quả sẽ tự động là Nothing, vì ta không có gì để landRight (-2) áp dụng với.

Ta không thể đạt được kết quả trên nếu chỉ sử dụng Maybe làm functor áp dụng. Nếu bạn thử viết, bạn sẽ bế tắc, vì các functor áp dụng không cho phép các giá trị áp dụng tương tác lẫn nhau. Cùng lắm là chúng có thể dùng được làm tham số cho hàm bằng cách dùng phong cách áp dụng thôi. Các toán tử áp dụng sẽ lấy những kết quả của chúng rồi đưa kết quả này vào cho hàm với hình thức phù hợp đối với từng giá trị áp dụng rồi hợp lại các giá trị áp dụng sau cùng với nhau, nhưng sẽ không có tương tác đáng kể giữa chúng. Trái lại, ở đây, mỗi bước lại phụ thuộc vào kết quả của bước liền trước. Mỗi khi chim đậu, kết quả có thể xảy ra ở thời điểm trước được xem xét và trạng thái cân bằng của cây sào được kiểm tra. Điều này quyết định xem việc chim đậu có ứng với kết quả tính toán thành công (sào vẫn cân bằng) hay không.

Ta cũng có thể lập một hàm để phớt lờ số chim hiện có trên cây sào thăng bằng mà chỉ nhằm làm cho Pierre trượt ngã. Ta sẽ gọi hàm này là banana:

banana :: Pole -> Maybe Pole
banana _ = Nothing

Bây giờ ta có thể xâu chuỗi nó lại cùng với các hàm chỉ định cho chim đậu. Nó sẽ luôn làm cho người đi trên dây bị ngã, vì hàm banana này phớt lờ bất kì thứ gì được cấp cho nó và luôn trả lại thất bại trong tính toán. Hãy kiểm tra này:

ghci> return (0,0) >>= landLeft 1 >>= banana >>= landRight 1
Nothing

Giá trị Just (1,0) được đưa vào banana, nhưng nó tạo ra một Nothing, vốn khiến mọi thứ trả lại kết quả Nothing. Thật không may!

Thay vì tạo lập các hàm phớt lờ số liệu đầu vào và chỉ trả lại một giá trị monadic định trước, thì ta có thể dùng hàm >>, vốn có nội dung như sau:

(>>) :: (Monad m) => m a -> m b -> m b
m >> n = m >>= \_ -> n

Thông thường, việc truyền một giá trị nào đó vào hàm mà bản thân nó phớt lờ tham số và luôn trả lại một giá trị định trước thì sẽ luôn trả lại giá trị định trước đó. Tuy nhiên với các monad thì ta còn phải xét đến ngữ cảnh và ý nghĩa của chúng nữa. Sau đây là cách mà >> hoạt động với Maybe:

ghci> Nothing >> Just 3
Nothing
ghci> Just 3 >> Just 4
Just 4
ghci> Just 3 >> Nothing
Nothing

Nếu thay thế >> bằng >>= \_ ->, bạn sẽ dễ thấy được tại sao nó lại có hành vi như trên.

Ta có thể thay thế hàm banana trong chuỗi này với một >> và tiếp theo là một Nothing:

ghci> return (0,0) >>= landLeft 1 >> Nothing >>= landRight 1
Nothing

Thế đó, sự thất bại trong tính toán là rõ ràng và luôn được bảo đảm!

Cũng cần phải xem điều gì sẽ xảy ra nếu ta không khéo quyết định coi các giá trị Maybe như những giá trị kèm theo ngữ cảnh thất bại rồi đưa chúng vào trong hàm, như đã làm ở trên. Sau đây là hệ quả một loạt các lượt chim đậu:

routine :: Maybe Pole
routine = case landLeft 1 (0,0) of
    Nothing -> Nothing
    Just pole1 -> case landRight 4 pole1 of 
        Nothing -> Nothing
        Just pole2 -> case landLeft 2 pole2 of
            Nothing -> Nothing
            Just pole3 -> landLeft 1 pole3

john joe glanton

Ta để một con chim đậu vào đầu sào bên trái rồi kiểm tra khả năng tính toán thất bại và khả năng thành công. Trong trường hợp thất bại, ta trả lại một Nothing. Trong trường hợp thành công, ta để chim đậu lên đầu sào bên phải rồi lặp lại tính toán từ đầu. Việc chuyển đổi công đoạn lằng nhằng này về một chuỗi các ứng dụng monad bằng >>= là một ví dụ kinh điển cho thấy monad Maybe đã giúp ta tiết kiệm được nhiều thời gian khi phải thực hiện liên tiếp những tính toán có khả năng bị thất bại.

Lưu ý cách lập Maybe của >>= phản ánh chính xác logic nhìn nhận nếu một giá trị là Nothing và nếu đúng vậy, thì lập tức trả lại Nothing còn nếu không thì tiếp tục với giá trị bên trong Just.

Ở mục này, ta sẽ lấy một số hàm đã có và thấy rằng chúng sẽ hoạt động tốt hơn nếu các giá trị trả lại cho phép xét đến thất bại trong tính toán. Bằng cách biến những giá trị như vậy thành giá trị Maybe rồi thay thế những áp dụng thông thường bằng >>=, thì tự nhiên ta có một cơ chế xử lý những thất bại, vì >>= theo quy định phải bảo tồn ngữ cảnh của giá trị mà nó áp dụng cho hàm. Trong trường hợp này, ngữ cảnh nói rằng giá trị đang xét là giá trị kèm theo thất bại và vì vậy khi áp dụng hàm với những giá trị như vậy thì cũng phải tính đến khả năng thất bại.

Cách viết “do”

Trong Haskell, các monad hữu ích đến nỗi chúng có hẳn cú pháp riêng với tên gọi là cách viết (kí pháp) do. Ta đã bắt gặp cách viết do khi thực hiện I/O và nói rằng chúng dùng để nối nhiều thao tác I/O lại với nhau. À, hóa ra là cách viết do không chỉ dùng riêng cho IO, mà còn dùng được cho bất kì monad nào. Về nguyên tắc cách làm với monad cũng như vậy: xâu chuỗi các giá trị monad lại với nhau. Ta sẽ xem cách viết do hoạt động như thế nào và vì sao nó có ích.

Hãy xét ví dụ áp dụng monad thường gặp sau đây:

ghci> Just 3 >>= (\x -> Just (show x ++ "!"))
Just "3!"

Ta đã gặp ví dụ này rồi. Đưa một giá trị monad vào cho một hàm để trả lại một giá trị monad, không có gì là ghê gớm. Lưu ý rằng khi ta viết như thế này, x sẽ biến thành 3 bên trong lambda như thế nào. Một khi ta đã ở trong lambda đó rồi, thì nó chỉ là một giá trị thông thường thay vì một giá trị monad. Bây giờ, điều gì sẽ xảy ra nếu ta có một >>=
khác trong hàm đó? Hãy kiểm tra nhé:

ghci> Just 3 >>= (\x -> Just "!" >>= (\y -> Just (show x ++ y)))
Just "3!"

A, cách dùng >>= lồng ghép nhau! Ở lambda ngoài cùng, ta đưa Just "!" cho lambda \y -> Just (show x ++ y). Bên trong lambda này, y trở thành "!". x vẫn là 3 vì ta lấy nó từ lambda bên ngoài. Tất cả những điều này gợi cho tôi nhớ về biểu thức sau:

ghci> let x = 3; y = "!" in show x ++ y
"3!"

Sự khác biệt chủ yếu giữa hai ví dụ trên là các giá trị trong ví dụ thứ nhất đều là giá trị monad. Chúng là các giá trị có ngữ cảnh thất bại. Ta có thể thay thế bất kì giá trị nào trong số đó với giá trị thất bại:

ghci> Nothing >>= (\x -> Just "!" >>= (\y -> Just (show x ++ y)))
Nothing
ghci> Just 3 >>= (\x -> Nothing >>= (\y -> Just (show x ++ y)))
Nothing
ghci> Just 3 >>= (\x -> Just "!" >>= (\y -> Nothing))
Nothing

Ở dòng lệnh thứ nhất, việc đưa Nothing vào một hàm tự nhiên sẽ có được kết quả Nothing. Ở dòng lệnh thứ hai, ta đưa Just 3 vào một hàm và x trở thành 3, nhưng sau đó ta đưa một Nothing vào cho lambda phía trong và kết quả của điều này là Nothing, và khiến cho lambda phía ngoài cũng cho ra Nothing. Như vậy việc này cũng tựa như gán các giá trị vào các biến trong biểu thức let, chỉ khác là các giá trị ở đây là những giá trị monad.

Để minh họa rõ hơn về điểm này, ta hãy viết đoạn mã lệnh sau vào một tập tin lệnh và để cho từng giá trị Maybe chiếm nguyên một dòng:

foo :: Maybe String
foo = Just 3   >>= (\x ->
      Just "!" >>= (\y ->
      Just (show x ++ y)))

Để giúp ta khỏi phải viết tất cả những lambda khó chịu như vậy, Haskell cho phép dùng cách viết do. Nó cho phép ta viết lại đoạn mã trên như sau:

foo :: Maybe String
foo = do
    x <- Just 3
    y <- Just "!"
    Just (show x ++ y)

90s owl

Dường như ta đã giành được khả năng để tạm thời kết xuất các thứ từ giá trị Maybe mà không phải kiểm tra xem liệu giá trị MaybeJust hay Nothing ở mỗi bước tính. Thật là hay! Nếu bất kì giá trị nào mà ta cố thử kết xuất mà là Nothing, thì toàn bộ biểu thức do sẽ trả lại kết quả Nothing. Ta đang giật những giá trị (có thể đang tồn tại) rồi để cho >>= lo ngữ cảnh đi cùng với những giá trị này. Điều quan trọng là nhớ rằng các biểu thức do chỉ là cú pháp khác dùng để xâu chuỗi các giá trị monad.

Trong một biểu thức do, mỗi dòng là một giá trị monad. Để xem xét kết quả này, ta dùng <-. Nếu ta có một Maybe String và dùng <- để gắn nó với một biến, thì biến này sẽ là một String, cũng như khi ta dùng >>= để đưa các giá trị monad vào trong lambda. Giá trị monad cuối cùng trong một biểu thức do, như Just (show x ++ y) ở đây, không thể dùng được với <- để gắn kết quả của nó, vì điều này vô nghĩa khi ta dịch ngược biểu thức do về một chuỗi các áp dụng >>=. Thay vào đó, kết quả của nó là kết quả của giá trị monad đã được hợp lại, có xét đến thất bại khả dĩ của bất kì những giá trị monad trước đó.

Chẳng hạn, hãy xét dòng lệnh sau:

ghci> Just 9 >>= (\x -> Just (x > 8))
Just True

Vì tham số bên trái của >>= là một giá trị Just nên the lambda được áp dụng cho 9 và kết quả là Just True. Nếu viết lại bằng do, ta sẽ có đoạn mã lệnh:

marySue :: Maybe Bool
marySue = do 
    x <- Just 9
    Just (x > 8)

Nếu so sánh hai cách viết trên, ta dễ thấy được lý do mà kết quả của toàn bộ giá trị monad là kết quả của giá trị monad cuối cùng trong biểu thức do xâu chuỗi cùng với tất cả giá trị trước đó.

Quá trình cân bằng còn có thể được diễn đạt bằng cách viết do. landLeftlandRight nhận một số chim và một Pole rồi tạo ra một Pole được bọc trong Just, trừ khi người đi trên dây trượt chân; khi đó giá trị Nothing được tạo ra. Ta dùng >>= để xâu chuỗi những bước liên tiếp vì mỗi bước lại phụ thuộc vào bước liền trước nó và mỗi bước đã thêm vào một ngữ cảnh là thất bại có thể xảy ra. Sau đây là trường hợp hai con chim đậu vào bên trái và sau đó hai con chim đậu vào bên phải và một con đậu thêm vào bên trái cây sào:

routine :: Maybe Pole
routine = do
    start <- return (0,0)
    first <- landLeft 2 start
    second <- landRight 2 first
    landLeft 1 second

Ta hãy xem liệu anh ấy còn giữ thăng bằng được không:

ghci> routine
Just (3,2)

À vẫn thăng bằng! Tuyệt. Khi ta thực hiện quá trình này bằng cách viết hẳn ra >>=, thì ta thường nói đại loại như return (0,0) >>= landLeft 2, vì landLeft 2 là một hàm trả về một giá trị Maybe. Tuy vậy, với các biểu thức do thì mỗi dòng lệnh phải thể hiện một giá trị monad. Vì vậy ta phải truyền hẳn cái Pole trước đó vào cho các hàm landLeftlandRight. Nếu kiểm tra những biến mà ta gắn các giá trị Maybe vào, ta thấy start sẽ là (0,0), first sẽ là (2,0) và cứ như vậy.

Vì các biểu thức do được viết theo từng dòng, theo nhận định cá nhân, chúng có thể giống như mã lệnh trong ngôn ngữ mệnh lệnh. Nhưng điều cốt yếu là, chúng có tính tuần tự, vì mỗi giá trị trên từng dòng lại dựa vào kết quả của những giá trị trước đó, cùng với ngữ cảnh của chúng (mà ở đây là việc tính toán thành công hay thất bại).

Một lần nữa, hãy xem đoạn mã lệnh này sẽ trông như thế nào nếu ta không dùng đến khía cạnh monad của Maybe:

routine :: Maybe Pole
routine = 
    case Just (0,0) of 
        Nothing -> Nothing
        Just start -> case landLeft 2 start of
            Nothing -> Nothing
            Just first -> case landRight 2 first of
                Nothing -> Nothing
                Just second -> landLeft 1 second

Hãy thấy rằng trong trường hợp thành công, thì bộ nằm trong Just (0,0) trở thành start, kết quả của landLeft 2 start trở thành first, và cứ như vậy.

Nếu ta muốn ném vỏ chuối vào bước đi của Pierre bằng việc can thiệp vào khối lệnh do, ta có thể viết như sau:

routine :: Maybe Pole
routine = do
    start <- return (0,0)
    first <- landLeft 2 start
    Nothing
    second <- landRight 2 first
    landLeft 1 second

Khi viết một dòng lệnh trong khối do mà không gắn một giá trị monad vào với <-, thì điều này cũng giống như đặt >> vào sau giá trị mà ta muốn phớt lờ kết quả của nó. Ta xâu chuỗi giá trị monad nhưng phớt lờ kết quả của nó vì không quan tâm nó là gì; như vậy thì hay hơn là cách viết tương đương, _ <- Nothing.

Việc lựa chọn khi nào dùng do và khi nào dùng hẳn >>= là tuỳ thuộc vào bạn. Tôi nghĩ ràng ví dụ trên thiên về viết hẳn >>= vì mỗi bước lại phụ thuộc vào kết quả cụ thể của bước liền trước đó. Với khối lệnh do, ta phải viết cụ thể ra cây sào nào đang được xét, nhưng mỗi lần ta lại dùng đối tượng mà đã xét đến ngay trước đó. Dù vậy, việc này cũng giúp ta hiểu sâu hơn về cách viết do.

Trong cách viết do, khi gắn các giá trị monad vào với tên gọi, ta có thể tận dụng khớp mẫu, cũng như trong các biểu thức let và các tham số hàm. Sau đây là một ví dụ khớp mẫu trong một biểu thức do:

justH :: Maybe Char
justH = do
    (x:xs) <- Just "hello"
    return x

Ta dùng khớp mẫu để lấy kí tự thứ nhất từ chuỗi "hello" rồi biểu diễn nó như kết quả. Theo đó, justH được lượng giá thành Just 'h'.

Điều gì sẽ xảy ra nếu việc khớp mẫu này thất bại? Khi trong hàm xảy ra thất bại ở khớp mẫu, thì dạng mẫu tiếp theo sẽ được khớp. Nếu trong hàm, tất cả các mẫu đều bị bỏ sót thì sẽ có lỗi được tung ra và chương trình bị đổ vỡ. Ngược lại, nếu khớp được với mẫu tương ứng với từng trường hợp thất bại trong biểu thức let thì lỗi sẽ được xủ lý ngay, vì không hề có cơ chế lọt qua các dạng mẫu ở biểu thức let. Khi thất bại trong việc khớp mẫu xảy ra trong biểu thức do thì hàm fail sẽ được gọi. Nó thuộc về lớp Monad và hàm này cho phép khớp mẫu thất bại dẫn đến sự thất bại trong ngữ cảnh của monad đang xét, thay vì làm chương trình đổ vỡ. Hàm fail mặc định được lập như sau:

fail :: (Monad m) => String -> m a
fail msg = error msg

Vì vậy theo mặc định, hàm này sẽ làm chương trình đổ vỡ, nhưng các monad có bao hàm ngữ cảnh thất bại có thể xảy ra (như Maybe) thì thường tự lập ra các hàm riêng. Đối với trường hợp Maybe, hàm được lập như sau:

fail _ = Nothing

Nó phớt lwof thông báo lỗi và tạo ra Nothing. Như vậy khi việc khớp mẫu thất bại trong một giá trị Maybe viết theo cấu trúc do, thì toàn bộ giá trị tạo nên sẽ là Nothing. Thế này hay hơn là để cho chương trình đổ vỡ. Sau đây là một biểu thức do với một dạng mẫu dễ bị thất bại:

wopwop :: Maybe Char
wopwop = do
    (x:xs) <- Just ""
    return x

Việc khớp mẫu thất bại, và hậu quả cũng như việc cả dòng lệnh chứa dạng mẫu được thay thế bởi một Nothing. Ta hãy thử gõ vào:

ghci> wopwop
Nothing

Việc khớp mẫu thất bại đã gây ra thất bại trong ngữ cảnh của monad đang xét thay vì gây thất bại cho toàn chương trình, đây là một điều hay.

Monad danh sách

dead cat

Đến giờ, ta đã thấy các giá trị Maybe có thể được coi là giá trị với ngữ cảnh thất bại ra sao và ta có thể đưa việc xử lý thất bại vào trong mã lệnh như thế nào, qua cách dùng >>= để đưa chúng vào hàm. Trong mục này, ta sẽ xét đến cách dùng những khía cạnh monad của danh sách để đưa tính không tất định vào mã lệnh một cách rõ ràng và dễ đọc.

Ta đã đề cập đến cách mà danh sách biểu diễn các giá trị không tất định ra sau khi chúng được dùng với vai trò các áp dụng. Một giá trị như 5 thì có tính tất định. Nó chỉ có một kết quả và ta biết chính xác kết quả đó là gì. Mặt khác, một giá trị như [3,8,9] thì lại chứa vài kết quả khác nhau, vì vậy cái mà ta gọi là một giá trị này thực ra là nhiều giá trị cùng một lúc. Việc dùng dnah sách như những functor áp dụng sẽ cho thấy rõ tính không tất định này:

ghci> (*) <$> [1,2,3] <*> [10,100,1000]
[10,100,1000,20,200,2000,30,300,3000]

Tất cả những tổ hợp có thể của phép nhân những phần tử ở danh sách phía trái với các phần tử trong danh sách phía phải đã được đưa vào trong danh sách kết quả. Khi xử lý dữ liệu không tất định, ta có nhiều chọn lựa, vì vậy đơn giản là ta thử tất cả những lựa chọn đó, và kết quả sẽ cũng là một giá trị không tất định, chỉ có điều nó có nhiều kết quả hơn.

Ngữ cảnh không tất định này có thể biểu diễn đẹp đẽ bằng monad. Ta hãy tiếp tục tìm hiểu để thấy thực thể Monad dành cho danh sách sẽ ra sao:

instance Monad [] where
    return x = [x]
    xs >>= f = concat (map f xs)
    fail _ = []

return làm công việc giống như pure, vậy nên có lẽ ta dễ làm quen với return đối với danh sách. Hàm này nhận một giá trị và đặt nó vào trong một ngữ cảnh mặc định tối thiểu mà vẫn cho được ra giá trị đó. Nói cách khác, nó tạo ra kết quả là một danh sách chỉ chứa một giá trị đó. Điều này có ích khi ta muốn đơn thuần là gói một giá trị thường vào trong một danh sách để ta có thể tương tác với những giá trị không tất định.

Để hiểu được cách hoạt động của >>= đối với danh sách, tốt nhất là ta nhìn vào mã lệnh cụ thể để có được trực giác ban đầu. >>= là để lấy một giá trị kèm ngữ cảnh (một giá trị monad) và đưa nó vào hàm, vốn nhận giá trị thường và trả lại giá trị có ngữ cảnh. Nếu hàm đó chỉ tạo ra một giá trị thường thay vì giá trị có ngữ cảnh, thì >>= sẽ không thật hữu ích vì sau một lần sử dụng, ngữ cảnh sẽ mất đi. Dù sao, ta hãy thử đưa một giá trị không tất định vào một hàm:

ghci> [3,4,5] >>= \x -> [x,-x]
[3,-3,4,-4,5,-5]

Khi dùng >>= với Maybe, giá trị monad được đưa vào hàm trong khi vẫn để ý tới khả năng xảy ra thất bại. Ở đây, nó giúp ta để ý đến sự không tất định. [3,4,5] là một giá trị không tất định và ta đưa nó vào một hàm, vốn cũng trả lại một giá trị không tất định. Kết quả cũng không tất định, và nó cho thấy tất cả những giá trị cụ thể khi lấy các phần tử trong danh sách [3,4,5] và truyền chúng vào cho hàm \x -> [x,-x]. Hàm này nhận một số rồi trả lại hai kết quả: một số giữ nguyên và một số đảo dấu. Vì vậy, khi ta dùng >>= để đưa danh sách này vào trong hàm thì mỗi số được đảo dấu và đồng thời cũng được giữ nguyên. Cái x trong lambda nhận từng giá trị của danh sách được đưa vào.

Để thấy được điều này đạt được ra sao, ta chỉ cần dò theo cách lập hàm. Trước hết, ta khởi đầu với danh sách [3,4,5]. Sau đó ta ánh xạ lambda lên nó và kết quả như sau:

[[3,-3],[4,-4],[5,-5]]

Lambda này được áp dụng cho từng phần tử và ta thu được một danh sách gồm các danh sách. Sau cùng, ta chỉ việc làm phẳng danh sách và đây rồi! Ta đã áp dụng một hàm không tất định cho một giá trị không tất định!

Sự không tất định cũng bao gồm việc hỗ trợ cho thất bại. Danh sách rỗng[] thì tương đương với Nothing, vì nó chỉ định sự vắng mặt của kết quả. Đó là nguyên nhân tại sao việc thất bại chỉ được định nghĩa như là một danh sách rỗng. Lời thông báo lỗi bị bỏ đi. Ta hãy nghịch chơi với những danh sách gây ra thất bại tính toán:

ghci> [] >>= \x -> ["bad","mad","rad"]
[]
ghci> [1,2,3] >>= \x -> []
[]

Ở dòng lệnh thứ nhất, một danh sách rỗng được đưa vào lambda. Vì danh sách không có phần tử nào nên chẳng có gì được chuyển vào hàm và vì vậy, kết quả là một danh sách rỗng. Điều này tương tự với việc đưa Nothing vào cho một hàm. Ở dòng lệnh thứ hai, từng phần tử được đưa vào hàm, nhưng phần tử bị phớt lờ và hàm chỉ trả lại một danh sách rỗng. VÌ hàm thất bại với từng phần tử được đưa vào nó nên kết quả là một thất bại.

Cũng giống như với các giá trị Maybe, ta có thể xâu chuỗi vài danh sách với >>=, từ đó lan truyền sự không tất định:

ghci> [1,2] >>= \n -> ['a','b'] >>= \ch -> return (n,ch)
[(1,'a'),(1,'b'),(2,'a'),(2,'b')]

concatmap

Danh sách [1,2] được gắn với n còn ['a','b'] được gắn với ch. Sau đó, ta viết return (n,ch) (hoặc [(n,ch)]), có nghĩa là lấy một cặp (n,ch) và đặt nó vào ngữ cảnh tối thiểu mặc định. Trong trường hợp này, nó đã tạo ra danh sách nhỏ nhất có thể nhưng vẫn biểu diễn cho kết quả (n,ch) và thể hiện càng ít sự không tất định càng tốt. Ảnh hưởng của nó đến ngữ cảnh là ở mức tối thiểu. Điều chúng ta đang xét đến ở đây là: với từng phần tử trong [1,2], duyệt qua mỗi phần tử trong ['a','b'] rồi tạo ra một bộ chứa một phần tử trong mỗi danh sách.

Nói chung, bởi vì return nhận một giá trị rồi gói nó lại vào một ngữ cảnh tối thiểu, nên nó không có bất kì hiệu ứng phụ nào (như thất bại trong Maybe hoặc trả lại kết quả có tính không tất định cao hơn, đối với danh sách), mà lại biểu diễn một kết quả gì đó.

Khi bạn có tương tác giữa những giá trị không tất định, bạn có thể coi các đại lượng của chúng như một cây trong đó mỗi kết quả có thể trong danh sách được biểu diễn bởi một cành riêng biệt.

Sau đây là biểu thwusc nói trên viết lại theo khối lệnh do:

listOfTuples :: [(Int,Char)]
listOfTuples = do
    n <- [1,2]
    ch <- ['a','b']
    return (n,ch)

Cách này đã rõ hơn một chút rằng n nhận từng giá trị từ [1,2]ch nhận từng giá trị từ ['a','b']. Cũng như với Maybe, ta đang kết xuất các phần tử từ những giá trị monad và dùng chúng như những giá trị thường, còn dấu >>= giúp ta lo liệu ngữ cảnh. Trong trường hợp này, ngữ cảnh là sự không tất định.

Việc dùng danh sách với cách viết do thực sự gợi tôi nhớ về một điều gì đó mà ta đã gặp từ trước. Hãy kiểm tra đoạn mã lệnh sau:

ghci> [ (n,ch) | n <- [1,2], ch <- ['a','b'] ]
[(1,'a'),(1,'b'),(2,'a'),(2,'b')]

Đúng rồi! Dạng gộp danh sách! Trong ví dụ nói trên về cách viết do, n trở thành mỗi kết quả từ [1,2] và với từng kết quả như vậy, ch được gán với một kết quả từ ['a','b'] và rồi dòng lệnh cuối cùng đã đặt (n,ch) vào trong một ngữ cảnh mặc định (một danh sách đơn phần tử) để biểu diễn nó như kết quả mà không giới thiệu bất kì sự không tất định nào khác. Trong dạng gộp danh sách này, điều tương tự đã diễn ra, chỉ khác là ta không phải viết return ở cuối để biểu diễn (n,ch) như một kết quả, vì phần đầu ra của dạng gộp danh sách đã giúp ta làm điều đó.

Thực ra, dạng gộp danh sách chỉ là cách viết tiện lợi cho việc dùng danh sách như những monad. Cuối cùng, dạng gộp danh sách và danh sách trong khối lệnh do được chuyển đổi về việc dùng >>= để thực hiện tính toán có sự không tất định.

Dạng gộp danh sách cho phép ta lọc kết quả đầu ra thu được. Chẳng hạn, ta có thể lọc một danh sách các cón số để tìm ra chỉ những số nào có chứa chữ số 7:

ghci> [ x | x <- [1..50], '7' `elem` show x ]
[7,17,27,37,47]

Ta áp dụng show cho x để biến đổi con số hiện có thành một chuỗi rồi kiểm tra xem liệu kí tự '7' có nằm trong chuỗi không. Thật khéo léo. Để thấy được việc lọc trong dạng gộp danh sách chuyển về monad danh sách như thế nào, ta cần kiểm tra hàm guard và lớp MonadPlus. Lớp MonadPlus là dành cho những monad nào có thể đóng vai trò như monoid. Sau đây là lời định nghĩa của nó:

class Monad m => MonadPlus m where
    mzero :: m a
    mplus :: m a -> m a -> m a

mzero tương đồng với mempty thuộc lớp Monoid còn mplus tương ứng với mappend. Vì danh sách cũng vừa là monoid, vừa là monad, chúng có thể được làm thành thực thể của lớp này:

instance MonadPlus [] where
    mzero = []
    mplus = (++)

Đối với danh sách, mzero biểu diễn một đại lượng không tất định mà không có bất kì kết quả nào — một đại lượng biểu diễn thất bại. mplus kết hợp hai giá trị không tất định lại làm một. Hàm guard được định nghĩa như sau:

guard :: (MonadPlus m) => Bool -> m ()
guard True = return ()
guard False = mzero

Nó nhận một giá trị boole và trong trường hợp đó là True, thì nhận vào một () rồi đặt nó vào trong một ngữ cảnh mặc định tối thiểu mà vẫn đảm bảo thành công. Còn nếu không, thì tạo ra một giá trị monad biểu diễn thất bại. Sau đây là cách dùng hàm này:

ghci> guard (5 > 2) :: Maybe ()
Just ()
ghci> guard (1 > 2) :: Maybe ()
Nothing
ghci> guard (5 > 2) :: [()]
[()]
ghci> guard (1 > 2) :: [()]
[]

Trông cũng hay đấy, nhưng hàm này có ích gì? Trong monad danh sách, ta dùng nó để lọc từ những đại lượng không tất định. Xem này:

ghci> [1..50] >>= (\x -> guard ('7' `elem` show x) >> return x)
[7,17,27,37,47]

Kết quả ở đây cũng giống như kết quả của dạng gộp danh sách trước đó. Bằng cách nào mà guard đạt được điều này? Trước hết ta hãy xem bằng cách nào mà các hàm guard kết hợp với >>:

ghci> guard (5 > 2) >> return "cool" :: [String]
["cool"]
ghci> guard (1 > 2) >> return "cool" :: [String]
[]

Nếu guard được thực hiện thành công thì kết quả chứa trong nó là một bộ rỗng. Vì vậy khi này ta dùng >> để phớt lờ bộ rỗng đó và biểu diễn kết quả là thứ gì đó khác. Tuy nhiên, nếu guard thất bại, thì các return sau đó cũng vậy, bởi việc đưa một danh sách rỗng vào trong hàm bằng >>= luôn trả lại kết quả là một danh sách rỗng. Về cơ bản, một guard nói rằng: nếu giá trị boole này là False thì hãy tạo ra một thất bại ngay ở đây, còn không thì hãy tạo ra một kết quả tượng trưng là () bên trong nó. Tất cả việc làm này là nhằm cho phép việc tính toán được tiếp diễn.

Sau đây là ví dụ trước được viết lại bằng khối do:

sevensOnly :: [Int]
sevensOnly = do
    x <- [1..50]
    guard ('7' `elem` show x)
    return x

Nếu ta quên biểu diễn x là kết quả cuối cùng bằng cách dùng return, thì danh sách thu được sẽ chỉ là một danh sách chứa những bộ rỗng. Sau đây là ví dụ trên nhưng viết dưới hình thức dạng gộp danh sách:

ghci> [ x | x <- [1..50], '7' `elem` show x ]
[7,17,27,37,47]

Vì vậy việc lọc trong danh sách cũng giống như dùng guard.

Hành trình của quân Mã

Sau đây là một bài toán rất thích hợp với cách giải có dùng tính không tất định. Chẳng hạn có một bàn cờ vua và một quân Mã. Ta cần biết liệu quân Mã có đến được một ô nào đó sau ba nước đi hay không. Để biểu diễn vị trí, ta sẽ dùng một cặp gồm hai số. Con số thứ nhất dùng để chỉ hàng và con số thứ hai chỉ cột mà quân Mã đang đứng.

hee haw im a horse

Ta hãy tạo một kiểu tương đồng cho vị trí hiện tại cuả quân Mã trên bàn:

type KnightPos = (Int,Int)

Vì vậy, chẳng hạn ban đầu Mã đứng ở (6,2). Liệu nó có thể đến (6,1) sau đúng ba nước đi không? Ta hãy xem nhé. Nếu bắt đầu tại (6,2), nước đi tiếp theo sẽ là gì? Tôi biết rồi, nhưng còn tất cả những nước đi là gì? Trong tay ta có dữ liệu không tất định, vậy thay vì chọn một nước đi, ta hãy tất cả cùng một lúc. Sau đây là một hàm nhận vào vị trí của quân Mã rồi trả lại tất cả những nước đi tiếp theo cuả nó.

moveKnight :: KnightPos -> [KnightPos]
moveKnight (c,r) = do
    (c',r') <- [(c+2,r-1),(c+2,r+1),(c-2,r-1),(c-2,r+1)
               ,(c+1,r-2),(c+1,r+2),(c-1,r-2),(c-1,r+2)
               ]
    guard (c' `elem` [1..8] && r' `elem` [1..8])
    return (c',r')

Quân Mã có thể luôn đi sang ngang hoặc dọc một ô rồi rẽ dọc hoặc ngang hai ô (nhưng không thể cùng là ngang hết hoặc dọc hết). (c',r') nhận từng giá trị từ danh sách các nước đi và sau đó guard đảm bảo rằng nước đi mới, (c',r') vẫn còn nằm trong phạm vi bàn cờ. Nếu không thì nó sẽ tạo ra danh sách rỗng, vốn gây ra sự thất bại và return (c',r') sẽ không được tiến hành cho vị trí đó.

Hàm này cũng có thể viết mà không cần dùng danh sách với vai trò monad, nhưng ở đây ta làm thế chỉ để minh họa. Sau đây là hàm này được viết bằng filter:

moveKnight :: KnightPos -> [KnightPos]
moveKnight (c,r) = filter onBoard
    [(c+2,r-1),(c+2,r+1),(c-2,r-1),(c-2,r+1)
    ,(c+1,r-2),(c+1,r+2),(c-1,r-2),(c-1,r+2)
    ]
    where onBoard (c,r) = c `elem` [1..8] && r `elem` [1..8]

Cả hai hàm đều có tác dụng tương tự, bạn có thể chọn cái nào “đẹp” hơn. Hãy thử nhé.

ghci> moveKnight (6,2)
[(8,1),(8,3),(4,1),(4,3),(7,4),(5,4)]
ghci> moveKnight (8,1)
[(6,2),(7,3)]

Chạy thật mê li! Nói nôm na là ta nhận vào một vị trí rồi xét đến tất cả các nước đi cùng lúc. Vì vậy, khi mà hiện giờ có được vị trí tiếp theo là không tất định, ta chỉ việc dùng >>= để đưa nó vào moveKnight. Sau đây là một hàm nhận vào một vị trí rồi trả lại tất cả các vị trí mà bạn có thể thu được sau ba nước nhảy Mã:

in3 :: KnightPos -> [KnightPos]
in3 start = do 
    first <- moveKnight start
    second <- moveKnight first
    moveKnight second

Nếu bạn truyền (6,2) vào hàm này, lượng kết quả thu được sẽ khá lớn, vì nếu có nhiều cách đi đến một vị trí nào đo trong ba nước thì vị trí đó sẽ có mặt trong danh sách nhiều lần. Cũng là hàm trên nhưng ko viết với khối lệnh do:

in3 start = return start >>= moveKnight >>= moveKnight >>= moveKnight

Dùng >>= một lần sẽ cho ta tất cả những nước đi có thể từ ban đầu và sau đó khi ta dùng >>= lần thứ hai, thì với mỗi nước đi thứ nhất được liệt kê, tất cả các nước tiếp theo hợp lệ sẽ được xét đến; và tương tự đối với nước cuối cùng.

Việc đặt một giá trị vào ngữ cảnh mặc định bằng cách áp dụng return cho nó rồi đưa nó vào trong một hàm bằng >>= thì cũng giống như với việc đơn thuần là áp dụng hàm cho giá trị đó, nhưng ta thực hiện cách đầu cho đúng với phong cách lập trình.

Bây giờ, ta hãy tạo một hàm nhận vào hai vị trí rồi báo cho ta biết liệu có thể đi từ một vị trí này sang vị trí khác sau đúng ba nước:

canReachIn3 :: KnightPos -> KnightPos -> Bool
canReachIn3 start end = end `elem` in3 start

Ta phát sinh tất cả những vị trí có thể trong ba bước rồi xét xem liệu vị trí mong muốn có nằm trong số đó không. Chẳng hạn, hãy xem liệu ta có thể đi từ (6,2) đến (6,1) sau ba nước hay không:

ghci> (6,2) `canReachIn3` (6,1)
True

Được! Vậy còn đi từ (6,2) đến (7,3)?

ghci> (6,2) `canReachIn3` (7,3)
False

Không! Bạn hãy thử làm bài tập sau: sửa lại hàm này sao cho khi bạn muốn đi từ một ô này sang ô khác thì nó sẽ báo cho bạn biết phải đi nước nào. Sau này, ta sẽ xem cách sửa đổi hàm trên để có thể truyền cả số nước đi cần thiết, thay là vì số cố định (3) như hiện giờ.

Các định luật đối với monad

the court finds you guilty of peeing all over everything

Cũng như đối với các functor áp dụng, và trước đó là các functor, luôn có một số định luật mà tất cả những thực thể monad phải tuân theo. Chỉ vì một thứ gì đó là thực thể của lớp Monad không có nghĩa rằng nó là một monad, mà chỉ có nghĩa là nó được được lập nên là thực thể của lớp đó. Để một kiểu dữ liệu thực sự trở thành monad thì kiểu đó phải thỏa mãn các định luật monad. Những định luật này cho phép ta đặt nên những giả định hợp lý về kiểu dữ liệu cùng hành vi của nó.

Haskell cho phép bất kì kiểu dữ liệu nào thuộc về một kiểu dữ liệu bất kì, miễn là các kiểu dữ liệu kiểm tra đạt yêu cầu (thỏa mãn các định luật monad). Vì vậy nếu ta tạo ra một thực thể mới thuộc lớp Monad, ta phải chắc rằng kiểu dữ liệu này đảm bảo được hết các định luật. Ta có thể dựa trên các kiểu dữ liệu đi theo thư viện chuẩn, chúng đều thỏa mãn các định luật. Nhưng sau này khi tự tạo ra các monad, ta sẽ phải tự tay kiểm tra xem các định luật đó có thỏa mãn không. Nhưng đừng lo, việc này không quá phức tạp.

Đơn vị trái

(Định nghĩa đơn vị trái và đơn vị phải ở Wikipedia.) Định luật monad thứ nhất phát biểu rằng nếu ta nhận một giá trị, đặt nó vào trong một ngữ cảnh mặc định bằng return rồi đưa nó vào một hàm bằng >>=, thì cũng giống như là chỉ việc lấy giá trị rồi áp dụng hàm lên nó. Nói một cách chặt chẽ:

  • return x >>= f cũng chẳng khác gì f x

Nếu bạn coi các giá trị monad như những giá trị với ngữ cảnh còn return như là việc lấy một giá trị rồi đặt nó vào trong ngữ cảnh tối thiểu mặc định mà vẫn biểu thị giá trị đó như là kết quả, thì sẽ có lý, vì nếu ngữ cảnh đó thực sự là tối thiểu, thì việc đưa giá trị monad này vào trong một hàm sẽ phải giống với việc chỉ áp dụng hàm cho giá trị thường, và thực sự hoàn toàn không có khác biệt nào.

Đối với monad Maybe thì return được định nghĩa là Just. Monad Maybe dành trọn cho khả năng xảy ra thất bại trong tính toán, và nếu ta có một giá trị và muốn đặt nó vào trong ngữ cảnh như vậy, thì sẽ có lý nếu ta coi nó như là một đại lượng tính toán thành công vì thật sự ta đã biết rằng giá trị đó là gì rồi. Sau đây là một số cách sử dụng return với Maybe:

>
ghci> return 3 >>= (\x -> Just (x+100000))
Just 100003
ghci> (\x -> Just (x+100000)) 3
Just 100003

Đối với monad danh sách, return đặt một thứ gì đó vào trong danh sách đơn phần tử. Nội dung của toán tử >>= với danh sách là duyệt qua tất cả những giá trị trong danh sách đó rồi áp dụng hàm lên chúng, nhưng vì chỉ có một phần tử trong danh sách đơn nên việc này cũng giống như là áp dụng hàm lên giá trị đó:

ghci> return "WoM" >>= (\x -> [x,x,x])
["WoM","WoM","WoM"]
ghci> (\x -> [x,x,x]) "WoM"
["WoM","WoM","WoM"]

Ta nói rằng đối với IO, việc dùng return tạo ra một thao tác I/O không chứa hiệu ứng phụ nhưng vẫn biểu diễn kết quả là một giá trị. Vì vậy sẽ định luật này cũng đúng với IO là điều hợp lý.

Đơn vị phải

Định luật thứ hai phát biểu rằng nếu ta có một giá trị monad và dùng >>= để đưa nó vào cho return, thì kết quả sẽ là giá trị monad ban đầu ta có. Nói một cách chặt chẽ:

  • m >>= return cũng chẳng khác gì m

Định luật này có thể khó thấy hơn định luật thứ nhất, nhưng hãy nhìn kĩ xem lý do tại sao nó phải được thỏa mãn. Khi ta đưa giá trị monad vào cho hàm bằng cách dùng >>=, các hàm đó nhận những giá trị thông thường và trả lại các giá trị monad. return cũng là một hàm như vậy, nếu xét đến kiểu của nó. Như ta đã nói, return đặt một giá trị vào trong ngữ cảnh tối thiểu mà vẫn biểu diễn kết qảu là giá trị dó. ĐIều này nghĩa là, chẳng hạn đối với Maybe, nó không giới thiệu bất cứ thất bại nào; và đối với danh sách, nó không giới thiệu bất cứ đại lượng không tất định nào. Sau đây là mã lệnh chạy thử một số monad:

ghci> Just "move on up" >>= (\x -> return x)
Just "move on up"
ghci> [1,2,3,4] >>= (\x -> return x)
[1,2,3,4]
ghci> putStrLn "Wah!" >>= (\x -> return x)
Wah!

Nếu nhìn kĩ hơn vào ví dụ danh sách này, thì cách tạo lập >>= là:

xs >>= f = concat (map f xs)

Vì vậy khi ta đưa [1,2,3,4] vào cho return, đầu tiên return được ánh xạ lên [1,2,3,4], được kết quả là [[1],[2],[3],[4]] rồi sau đó kết quả này được nối lại và ta lại được danh sách ban đầu.

Các đơn vị trái và đơn vị phải là những định luật cơ bản diễn tả hành vi mà return cần có. Đó là một hàm quan trọng để biến các giá trị thường thành các giá trị moand và sẽ không hay nếu giá trị monad được tạo ra phải làm nhiều công việc khác nữa.

Tính kết hợp

Định luật monad cuối cùng phát biểu rằng khi ta có một dãy các áp dụng hàm có tính monad, nối bởi >>=, thì kết quả chẳng bị ảnh hưởng bởi thứ tự lồng ghép. Nói một cách chặt chẽ:

  • Việc viết (m >>= f) >>= g cũng giống như m >>= (\x -> f x >>= g)

Hừm, bây giờ điều gì đang diễn ra thế này? Ta có một giá trị monad, m và hai hàm monad fg. Khi viết (m >>= f) >>= g, ta đã đưa m vào cho f, vốn trả lại kết quả là một giá trị monad. Sau đó, ta đưa giá trị monad này vào cho g. Trong biểu thức m >>= (\x -> f x >>= g), ta lấy một giá trị monad và đưa nó vào trong một hàm vốn có nhiệm vụ đưa kết quả của f x vào cho g. Thật không dễ thấy rằng hai cái trên tương đương nhau như thế nào, vậy ta hãy xét một ví dụ để thấy được điều này rõ hơn.

Bạn còn nhớ Pierre người đi trên dây với lũ chim đậu trên cây sào thăng bằng chứ? Để mô phỏng những con chim đậu trên cây sào, ta lập một dãy các hàm có khả năng gây thất bại tính toán:

ghci> return (0,0) >>= landRight 2 >>= landLeft 2 >>= landRight 2
Just (2,4)

Ta bắt đầu với Just (0,0) rồi gắn giá trị đó vào hàm monad tiếp theo, landRight 2. Kết quả đó là một giá trị monad khác và lại được gắn vào hàm monad kế tiếp, rồi cứ như vậy. Nếu muốn viết hẳn các cặp ngoặc đơn thì ta đã có:

ghci> ((return (0,0) >>= landRight 2) >>= landLeft 2) >>= landRight 2
Just (2,4)

Nhưng ta cũng có thể viết đoạn mã như sau:

return (0,0) >>= (\x ->
landRight 2 x >>= (\y ->
landLeft 2 y >>= (\z ->
landRight 2 z)))

return (0,0) cũng giống như Just (0,0) và khi ta đưa nó vào lambda, thì x trở thành (0,0). landRight nhận một số chim và một cây sào (một bộ các số) rồi đó là những gì truyền đến nó. Kết quả là Just (0,2) và khi ta đưa kết quả này vào lambda kế tiếp, y bằng (0,2). Cứ thế đến khi con chim cuối cùng đậu và kết quả là Just (2,4), cũng chính là kết quả của cả biểu thức lớn.

Như vậy bất kể bạn lồng ghép các giá trị được truyền vào hàm monad thế nào, thì chẳng ảnh hưởng gì; điều ảnh hưởng là ý nghĩa của bản thân chúng. Sau đây là cách hiểu khác về định luật này: xét việc hợp hai hàm, fg. Việc hợp hai hàm được thực hiện như sau:

(.) :: (b -> c) -> (a -> b) -> (a -> c)
f . g = (\x -> f (g x))

Nếu như kiểu của ga -> b và kiểu của fb -> c, ta sắp xếp chúng vào trong một hàm mới có kiểu là a -> c, để cho tham số của nó được truyền giữa những hàm đó. Bây giờ điều gì sẽ xảy ra nếu hai hàm nói trên là monad, nghĩa là nếu các giá trị mà chúng trả lại đều là giá trị monad? Nếu ta có một hàm thuộc kiểu a -> m b, thì ta không thể đơn giản là truyền kết quả của nó cho một hàm thuộc kiểu b -> m c, vì hàm đó chấp nhận một giá trị b thông thường chứ không phải monad. Tuy vậy, ta có thể dùng >>= để khiến điều này xảy ra. Như vậy là bằng cách dùng >>=, ta có thể hợp hai hàm monad:

(<=<) :: (Monad m) => (b -> m c) -> (a -> m b) -> (a -> m c)
f <=< g = (\x -> g x >>= f)

Như vậy giờ ta đã có thể hợp hai hàm monad:

ghci> let f x = [x,-x]
ghci> let g x = [x*3,x*2]
ghci> let h = f <=< g
ghci> h 3
[9,-9,6,-6]

Hay đấy. Vậy điều này thì liên quan gì đến định luật kết hợp? À, khi ta nhìn định luật dưới hình thức một định luật về hàm hợp thì nó phát biểu rằng f <=< (g <=< h) phải tương đương với (f <=< g) <=< h. Đây là một cách nói khác rằng đối với monad, việc lồng ghép các toán tử chẳng ảnh hưởng gì.

Nếu ta chuyển đổi hai định luật đầu về cách dùng <=<, thì định luật đơn vị trái phát biểu rằng với mỗi hàm monad f, thì f <=< return cũng tương đương với chỉ viết f và định luật đơn vị phải phát biểu rằng return <=< f thì cũng không khác gì f.

Điều này giống với nhận định là nếu f là một hàm thông thường thì (f . g) . h cũng giống như f . (g . h), f . id luôn giống như fid . f cũng giống như f.

Trong chương này, ta đã xem xét kiến thức cơ bản về monad và học cách hoạt động của monad Maybe và monad danh sách. Trong chương tiếp theo, ta sẽ xét đến một loạt những monad hay khác và ta cũng sẽ học cách tự tạo các monad riêng.

Advertisements

2 phản hồi

Filed under Haskell

2 responses to “Chương 12: Một số vấn đề về Monad

  1. Pingback: Tự học lấy Haskell | Blog của Chiến

  2. Pingback: Chương 14: Khóa kéo | Blog của Chiến

Trả lời

Mời bạn điền thông tin vào ô dưới đây hoặc kích vào một biểu tượng để đăng nhập:

WordPress.com Logo

Bạn đang bình luận bằng tài khoản WordPress.com Log Out / Thay đổi )

Twitter picture

Bạn đang bình luận bằng tài khoản Twitter Log Out / Thay đổi )

Facebook photo

Bạn đang bình luận bằng tài khoản Facebook Log Out / Thay đổi )

Google+ photo

Bạn đang bình luận bằng tài khoản Google+ Log Out / Thay đổi )

Connecting to %s