Trong lập trình ta sẽ bắt gặp hai loại function: sync (đồng bộ - synchronous) và
async (bất đồng bộ - asynchronous). Với sync function, chương trình phải đợi
từng tác vụ hoàn thành trước khi bắt đầu một tác vụ khác. Và, ngược lại, với
async function, ta có thể thực hiện nhiều tác vụ một cách đồng thời.
Đối với Javascript, để gọi một async function, lập trình viên thường phải sử
dụng continuation-passing style, truyền callback mỗi khi gọi function,
dẫn đến một thứ gọi là callback hell.
Sử dụng promise cùng tư duy functional programming sẽ giúp lập trình
viên dễ dàng nắm bắt được luồng xử lý của chương trình khi dùng async function.
II. Async với callback
Chắc hẳn mọi người đều biết đến những đoạn code kiểu này:
Với phương pháp này, mỗi khi gọi một async function, lập trình viên truyền thêm
một tham số: một function được gọi khi async function kia kết thúc.
Ở đây, ta không quan tâm giá trị getJson trả về mà chỉ quan tâm đến side
effect: đó là việc callback mà ta truyền vào được gọi. Mỗi khi sử dụng một
async function như getJson, luồng thực hiện cũng như luồng tư duy của lập
trình viên bị “ngắt quãng”. Để hiểu được chương trình, lập trình viên cần xem
kết quả trả về được sử dụng như thế nào trong tương lai ở hàm callback rồi
lại tiếp tục với các dòng lệnh tiếp theo. Khi đọc và viết code, lập trình viên
chạy chương trình trong đầu, vậy nên khi sử dụng nhiều callback, đặc biệt là
callback lồng nhau thì chương trình trở nên cực kì khó đọc.
Xét một ví dụ chương trình đơn giản mô phỏng việc mua gạo, thực hiện từng bước
như sau:
Nếu dùng callback thì ta sẽ có:
Với một loạt callback lồng nhau, chương trình trở nên khó đọc và khó hiểu.
III. I promise it will help
Trong phần này, ta cùng xem sử dụng promise giúp cải thiện đoạn chương trình
trên như thế nào.
1. Điểm lại một số khái niệm về functional programming
Functor hay Monad đều có thể xem như giá trị gắn kèm thêm ngữ cảnh (có thể
đọc thêm về monad ở bài viết trước). Các kiểu dữ liệu này ta có thể tưởng
tượng như một hộp chứa dữ liệu bên trong.
Với Functor, ta có thể map các giá trị bên trong chiếc hộp này.
Type signature: map :: (a -> b) -> Functor a -> Functor b
Ví dụ:
Monad thì sẽ là flatMap
Type signature: flatMap :: Monad a -> (a -> Monad b) -> Monad b
Ví dụ:
2. Promise và functional programming
Ta kí kiệu Promise[T] là một Promise bọc bên ngoài một giá trị có kiểu là
T. Khi một function trả về Promise[T], ta hiểu function đó muốn nói rằng “À,
hiện giờ tôi chưa thể trả về kết quả ngay được, nhưng tôi hứa sau này sẽ trả
về kết quả kiểu T, trừ khi có lỗi xảy ra”.
Với kết quả trả về là Promise[T] thì sẽ có 2 khả năng xảy ra:
Mọi chuyện đều êm đẹp, ta thu được giá trị kiểu T
Có lỗi xảy ra, ta thu được thông báo error
Ta cùng xét xem map và flatMap đối với Promise[T] như thế nào nhé!
Ở đây nếu promise resolve về một giá trị data thì function map sẽ trả về
một promise resolve về giá trị transform(data). Và ngược lại, nếu promise
có lỗi error, thì kết quả của map vẫn chính là promise này.
Có thể thấy, flatMap giống hệt map bên trên. Lí do là ở function truyền vào
onFulfilled của promise.then, nếu function này trả về một giá trị nào đó thì
kết quả thu được của promise.then sẽ là một promise được fulfilled bởi giá
trị vừa thu được; còn nếu onFulfilled trả về một promise, thì kết quả của
promise.then sẽ chính là promise vừa được trả về.
3. Thử dùng promise
Chương trình “nấu cơm ăn” bên trên nếu viết dưới dạng functional programming sẽ
có dạng như sau:
Áp dụng với promise cùng map và flatMap bên trên ta được:
Kết quả cuối cùng ta có chương trình
Toàn bộ code của bài viết có thể xem tại hai link jsfiddle sau: