Lưu ý: Trước khi tiếp tục đọc bài viết này tôi khuyên bạn nên tìm hiểu trước về Curry function là gì? Một món "cà ri" ngon và làm sao để thưởng thức nó? hoặc nếu đã biết về curry function bạn có thể tiếp tục đọc bài viết.
Composition là một cơ chế kết hợp nhiều hàm đơn giản để xây dựng một hàm phức tạp hơn. Kết quả của mỗi hàm sẽ được chuyển cho hàm tiếp theo.
Nó giống như trong toán học, chúng ta có một hàm số f(g(x))
, tức là kết quả của g(x)
được chuyển cho hàm f
. Thì composition là như vậy.
Một ví dụ đơn giản: Viết hàm thực hiện phép tính 1 + 2 * 3.
Đối với phép tính này chúng ta phải thực hiện phép nhân trước sau đó đến phép cộng. Đoạn mã khi được triển khai bằng các hàm trong Javascript sẽ trông như thế này:
const add = (a, b) => a + b;
const mult = (a, b) => a * b;
add(1, mult(2, 3));
Oh! hàm chạy rất tốt tuy nhiên có hơi rối một chút nhỉ. Giả sử bây giờ tôi muốn chia tất cả cho 4 thì sao? Một đoạn mã gì đó sẽ trông như thế này:
div(add(1, mult(2, 3)), 4);
Chà rối hơn rồi đấy!
Bây giờ chúng ta sẽ đi đến một ví dụ khác. Giả sử tôi có một danh sách users
bao gồm tên và tuổi, hãy lấy ra tên của những người trên 18 tuổi. Đoạn mã đó sẽ giống như:
const users = [
{ name: "A", age: 14 },
{ name: "B", age: 18 },
{ name: "C", age: 22 },
];
const filter = (cb, arr) => arr.filter(cb);
const map = (cb, arr) => arr.map(cb);
map(u => u.name, filter(u => u.age > 18, users)); // ["C"]
Tư tưởng là tôi sẽ tạo ra 2 hàm filter
& map
, filter
để lọc còn map là để duyệt
qua các phần tử. Đoạn mã trên hoạt động tốt tuy nhiên cũng như ví dụ đầu tiên, nó có hơn rườm rà một chút.
Vậy thì có cách nào giải quyết được ổn thoả hai vấn đề trên? Hoặc chí ít là giúp cho mã rõ ràng hơn khi điều kiện bài toán tăng thêm.
compose
Mục tiêu của tôi là sẽ tạo ra một hàm nhận vào nhiều tham số, các tham số này là những hàm nhỏ hơn để thực hiện một khối lượng công việc nhất định (Higher Order Function).
Nó sẽ trông giống như là hàm compose
này:
compose(function1, function2…, functionN): Function
compose
nhận vào các hàm và trả ra một hàm. Tư tưởng của compose
là khi nó được gọi, nó sẽ thực hiện các hàm trong tham số từ phải sang trái, kết quả của hàm trước sẽ được chuyển thành đối số của hàm sau.
Đây là một cách đơn giản để implement hàm compose
bằng ES6:
const compose = (...functions) => args => functions.reduceRight((arg, fn) => fn(arg), args);
Sẽ thật là hạnh phúc với tôi nếu bạn hiểu được những gì bên trong compose
thực sự làm, còn nếu không hiểu thì hãy comment ở phía dưới bài viết nhé.
Bây giờ quay trở lại với ví dụ ban đầu, ta hãy sửa lại mã một chút:
const add = a => b => a + b;
const mult = a => b => a * b;
const operator = compose(add(1), mult(2));
const result = operator(3);
// Hoặc ngắn gọn hơn chúng ta cũng có thể viết
const result = compose(add(1), mult(2))(3);
Tôi đã biến add
và mult
thành hàm curry, bời vì sao? Bởi vì khi chuyển nó thành curry tôi có thể dùng nó như là một tham số là hàm vào trong compose
.
Được rồi bây giờ muốn tất cả chia cho 4 thì sao?
const div = a => b => b / a;
const result = compose(div(4), add(1), mult(2))(3);
Thật dễ đọc phải không. Từ trái sang phải lần lượt thực hiện nhân với 2, sau đó cộng thêm 1 và cuối cùng chia cho 4. Cứ giống như một dòng chảy vậy.
Tương tự như vậy với ví dụ 2 hãy sửa lại mã của nó một chút:
const users = [
{ name: "A", age: 14 },
{ name: "B", age: 18 },
{ name: "C", age: 22 },
];
const filter = cb => arr => arr.filter(cb);
const map = cb => arr => arr.map(cb);
compose(
map(u => u.name),
filter(u => u.age > 18),
)(users); // ["C"]
Chúng ta có thể viết thêm một cơ số hàm nối tiếp ở trong compose
mà vẫn giữ được dòng chảy dữ liệu hoạt động, và hơn hết là giữ cho đoạn mã tương đối dễ đọc.
pipe
Tương tự như compose
, pipe
cũng mang tư tưởng giống như compose
chỉ duy nhất khác một điều là thứ tự thực hiện các hàm trong tham số là từ trái sang phải.
Hàm pipe
sẽ được implement như thế này:
const pipe = (...functions) => args => functions.reduce((arg, fn) => fn(arg), args);
Áp dụng pipe
vào ví dụ 1:
const add = a => b => a + b;
const mult = a => b => a * b;
const result = pipe(mult(2), add(1))(3);
Như các bạn thấy các tham số được truyền vào ngược lại so với compose
, nó sẽ thực hiện các hàm từ phải sang trái: nhân 3 với 2 rồi sau đó cộng 1.
Bạn có thể sử dụng compose
và pipe
tuỳ theo sở thích hoặc thói quen vì hai hàm đều mang lại kết quả tương tự nhau.
Hai hàm compose
và pipe
tuy nhỏ nhưng nó mang lại lợi ích rất lớn trong việc áp dụng vào các bài toán xử lý dữ liệu liên tục. Nó giúp mã rõ ràng & dễ đọc hơn.
Hầu như các thư viện Javascript hỗ trợ việc xử lý dữ liệu đều có sẵn các hàm tương tự như compose
hoặc pipe
như _.compose
, _.pipe
trong lodash
hay compose
, pipe
trong ramdajs
.
Hy vọng sau bài viết này các bạn sẽ có thêm một phương pháp để xử lý dữ liệu trong các dự án sắp tới. Ngay bây giờ nếu gặp các vấn đề tương tự như trên hãy thử áp dụng ngay bạn nhé!
Bí mật ngăn xếp của Blog
Là một lập trình viên, bạn có tò mò về bí mật công nghệ hay những khoản nợ kỹ thuật về trang blog này? Tất cả bí mật sẽ được bật mí ngay bài viết dưới đây. Còn chờ đợi gì nữa, hãy bấm vào ngay!
Đăng ký nhận thông báo bài viết mới
Bình luận (0)