Bài viết được sự cho phép của tác giả Tống Xuân Hoài
Đã bao giờ bạn gặp bài toán xử lý dữ liệu liên tục? Vậy thì bạn sẽ làm gì trong trường hợp
Giới thiệu về composition function
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.
Triển khai
Hà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é. Tôi sẽ theo dõi comment của bạn!
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.
Tham khảo việc làm JavaScript hấp dẫn trên TopDev
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.
Hàm 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.
Tổng kết
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
.
Bài viết gốc được đăng tải tại 2coffee.dev
Xem thêm:
- 7 khái niệm Javascript cơ bản không thể bỏ qua
- Shipit – Tự động deploy Javascript project
- 9 project nhỏ mà bạn có thể code để luyện tập kỹ năng lập trình
Đừng bỏ lỡ hàng loạt IT job hot tại TopDev