Functor, Applicative, Monad
Bài viết này được lược dịch lại từ bản tiếng Anh: Functors, Applicatives, And Monads In Pictures, giải thích khá dễ hiểu các khái niệm căn bản trong lập trình hàm(với ví dụ minh hoạ của ngôn ngữ Haskell)
Để bắt đầu trước tiên ta làm quen với khái niệm value và context
Value and Context
Trước hết, ta có một giá trị kiểu số, chẳng hạn như:
Khi áp dụng một function, chẳng hạn function cộng vào số trên ta được kết quả là một giá trị số khác.
Kí hiệu
(+3)
là kí hiệu cho hàmf(x) = x + 3
.Do đó
(+3) 2
tương đương vớif(2) = 2 + 3 = 5
Ta mở rộng khái niệm giá trị trên, cho nó vào một ngữ cảnh(context), có thể tưởng tượng như lấy giá trị đó bỏ vào một cái hộp
Khi áp dụng một function vào trong chiếc hộp này, ta nhận được kết quả khác nhau, tùy theo chiếc hộp và giá trị chứa bên trong nó.
Trước khi tiếp tục, ta làm quen với kiểu dữ liệu Maybe
. Định nghĩa của Maybe
trong Haskell như sau:
a
ở đây là một kiểu dữ liệu bất kì(Int, Float, Function,…). Chẳng hạn:
Maybe Int
, Maybe Float
, Maybe String
Maybe Int
sẽ có các giá trị: Nothing
, Just -1
, Just 0
,Just 1
, Just 100
…
Maybe Float
sẽ có các giá trị: Nothing
, Just 1.1
, Just 100.25
, …
Nothing
gần giống như khái niệmnull
trong các ngôn ngữ lập trình khác.
Sau đây ta sẽ làm quen với khái niệm Functor
Functor
Khi một giá trị được đặt trong một context(chẳng hạng Just 2
), ta không thể áp dụng hàm (+3)
như trên được.
Vì thế, xuất hiện hàm fmap
giúp ta làm được việc không thể trên.
fmap
sẽ lấy giá trị trong chiếc hộp đó ra và áp dụng function (+3)
, lấy kết quả bỏ vào hộp trở lại.
Thế nhưng, làm cách nào fmap
biết cách áp hàm (+3)
vào giá trị Just 2
?
Functor là một typeclass. Đây là định nghĩa của Functor
Để implement một kiểu dữ liệu f có typeclass là Functor ta định nghĩa một hàm
fmap
trên kiểu dữ liệu đó, thõa mãn:fmap :: (a -> b) -> f a -> f b
Có thể hiểu khái niệm typeclass giống như khái niệm abstract class/interface trong Java. Một kiểu dữ liệu có thể implement nhiều typeclass khác nhau.
Vì thế, một kiểu dữ liệu được gọi là Functor nếu kiểu dữ liệu đó định nghĩa một hàm fmap
Maybe
là một functor, do đó ta có thể gọi fmap
cho Maybe
như sau:
Maybe
định nghĩa fmap
như sau:
Trước khi tiếp tục, thử trả lời câu hỏi sau:
Áp một hàm vào Nothing
sẽ nhận được Nothing
, không còn gì hợp lí hơn!
Việc này giúp ta đỡ phải xét giá trị null
, chẳng hạn như câu if
sau trong Ruby:
chỉ cần 1 dòng trong Haskell:
<$>
là phiên bản trung tố của fmap
, ta có thể viết lại ví dụ trên như sau:
Trong Haskell, tên một function có thể được tạo thành từ các kí tự bất kì. Ví dụ ở trên ta đã biết
+
là một function nhận 2 tham số(+) 2 3 == 5
Hỏi thêm 1 câu hỏi nhỏ trước khi tiếp tục: khi áp dụng một function vào trong một List thông qua fmap
, ta sẽ được gì?
List cũng là một Functor:
Như vậy kết quả fmap
của một function và một List chính là hàm map của function trên List đó.
Ví dụ cuối cùng: kết quả của fmap
trên một function là gì?
Trả lời: kết quả là một function khác.
Đây là định nghĩa fmap
trên một function:
Trong Haskell, kí hiệu
.
có nghĩa là hàm hợp của hai function(lấy output của function này làm input của function kia)
Như vậy, fmap
của 2 function chẳng qua là phép hợp của 2 function đó.
Applicative
Trở lại với value và context:
Function của ta cũng được wrap trong một context:
Như thế làm sao có thể áp dụng function vào value?(rõ ràng fmap
không phát huy được tác dụng trong trường hợp này).
Tương tự như Functor, ta có typeclass Applicative để giải quyết vấn đề trên. Applicative định nghĩa hàm <*>
nhận vào tham số là một context function và một context value.
Ví dụ cho Just 2
và Just (+3)
:
Hay:
Câu hỏi: kết quả của biểu thức sau là gì?
Do đó:
Với việc kết hợp giữa Functor và Applicative, ta có thể áp dụng một function có 2 tham số vào 2 giá trị, chẳng hạn như ta có (+)
, Just 3
, Just 5
, làm sao để được Just 8
?
Chú ý trong Haskell, 2 cách viết sau là tương đương:
2 + 3 (+) 2 3
Haskell cung cấp hàm liftA2
để thực hiện công việc trên:
Monad
Hiện giờ, ta có 2 typeclass:
Functor: áp dụng cho function và context value
Applicative: áp dụng cho context function và context value
Monad áp dụng cho một function có kết quả trả về là một context value và một context value khác.
Ví dụ ta có hàm half
được định nghĩa như sau:
Thế nếu áp dụng một context value cho hàm half
thì sao?
typeclass Monad định nghĩa hàm >>=
thực hiện yêu cầu trên
Signature của >>=
như sau:
Maybe định nghĩa >>=
như sau:
Ta có thể kết hợp liên tiếp >>=
như sau:
Monad áp dụng cho datatype IO
Trong Haskell ta có hàm getLine
: nhận input từ console trả về một String
được gói trong context IO
Hàm readFile
: input một chuỗi(tên file) và trả về một String được gói trong context IO
Trong Haskell datatype FilePath chính là alias của String
Hàm putStrLn
: input một String
, in chuỗi đó ra console, trả về IO ()
Do IO
cũng là một Monad nên ta có thể áp dụng >>=
vào các function trên như sau:
Kết quả là chuỗi hành động sau:
- Nhận input file name từ console
- Đọc file đó
- In nội dung file đó ra console
Haskell cung cấp cú pháp do
để làm công việc nối nhiều function IO lại với nhau như trên.
Tóm lại
- Functor là là một kiểu dữ liệu định nghĩa typeclass Functor
- Applicative là kiểu dữ liệu định nghĩa typeclass Applicative
- Monad là kiểu dữ liệu định nghĩa typeclass Monad
Maybe
định nghĩa cả 3 typeclass Functor, Applicative, Monad. Do đó có thể dùngfmap
,<$>
,<*>
,>>=
vớiMaybe
Điểm khác biệt của các typeclass trên là gì?
- functor: áp dụng một function vào một context value thông qua hàm
fmap
hoặc<$>
- applicative: áp dụng một context function vào một context value thông qua hàm
<*>
- monad: áp dụng một function trả về context value vào một context value thông qua hàm
>>=