Cảm ơn bạn!
Compose và Pipe trong Javascript là gì? những lợi ích mà nó mang lại cho lập trình viên
Function Composition là gì?
Đây là cách chúng ta kết hợp nhiều function lại với nhau. Kết quả của function này sẽ được sử dụng cho function tiếp theo. Cứ như vậy nó sẽ tạo thành một chuỗi các function thực hiện các nhiệm vụ theo thứ tự.
Bằng cách chia để trị, Function Composition giúp code của chúng ta có thể dễ dàng maintain, reuse các đoạn code, quản lí input, output dễ dàng hơn, vì thế nó giúp cho việc debug trở nên đơn giản 😄.
Thực chất thì Compose và Pipe là một. Chúng chỉ khác nhau về thứ tự thực hiện các function. Compose sẽ thực hiện theo thứ tự Right to left và Pipe là Left to Right. Hơi trừu tượng nhỉ 🤨 cùng đến với một ví dụ và xem cách triển khai của chúng nhé.
Ví dụ
Đầu tiên để có thể hình dung được Compose & Pipe thì chúng ta hãy xem một ví dụ dưới đây:
Giả sử mình có 2 function sau:
function absolute(num) {
return Math.abs(num);
}
function multiplyBy2(num) {
return num * 2;
}
multiplyBy2(absolute(-5)); // 10
Ở hàm trên ta sử dụng kết quả của hàm trị tuyệt đối absolute()
để sử dụng cho hàm multiplyBy2()
cuối cùng ta được kết quả như mong đợi. Khá dễ dàng đúng không nào ^^. Bây giờ mình muốn thêm một chức năng tăng kết quả nhận được thêm 3 thì ta được như sau:
function plusBy3(num) {
return num + 3;
}
plusBy3(multiplyBy2(absolute(-5))); //13
Bạn thấy việc truyền kết quả của hàm này làm param cho hàm kế tiếp sẽ làm code của chúng ta nhìn khó hơn đúng không nào. tất nhiên đây là một ví dụ đơn giản để các bạn có thể tiếp cận dần đến Compose & Pipe nên chúng ta có thể không thấy nó khó nhìn ^^.
Bây giờ mình sẽ làm cho đoạn code trên nhìn thông thoáng và gọn hơn nhé. Mình sẽ sử dụng Compose trong ví dụ này.
Sử dụng Compose trong bài toán trên nhìn sẽ như thế này:
function calc(multiply, absolute) {
return (num) => multiply(absolute(num));
}
const value = calc(multiplyBy2, absolute)(-5);
console.log(value); // 10
Để mình giải thích một chút, đầu tiên chúng ta sẽ có một hàm dùng các hàm con để làm params là calc
. Kết quả của hàm này sẽ trả về một arrow function. Khi invoke (Gọi hàm) mình truyền -5
vào arrow function này. cuối cùng là nhiệm vụ của arrow function, trả về kết quả mà 2 hàm multiplyBy2
và absolute
đã return. Vẫn hơi khó hình dung đúng không.
hàm calc (Compose) sẽ thực thi các hàm theo thứ tự Right to Left nghĩa là:
Trị tuyệt đối của 5 > Nhân kết quả cho 2
Tại sao Compose lại theo thứ tự Right to Left ? Đơn giản thì các bạn hãy nhìn vào cách chúng ta gọi hàm multiply(absolute(num))
. Javascript sẽ thực thi hàm theo thứ tự hàm bên trong cùng rồi dần dần ra ngoài.
Ví dụ: multiplyBy3(absolute(divideBy2(-4)))
Thực hiện: chia -4 cho 2 > trị tuyệt đối > nhân kết quả cho 3.
Để tìm hiểu thêm các bạn có thể xem trong toán học, người ta có một biểu thức f(g(x)) trong đó kết quả của g(x) sẽ được sử dụng cho hàm f(). Xem tại đây .
Sau khi đã hình dung được cách thực thi hàm theo thứ tự của Compose (Right to left) và Pipe (Left to right), chúng ta sẽ đến với phần triển khai Compose theo cách tổng quát.
Một lưu ý cho các bạn đó là trong quá trình sử dụng Compose hoặc Pipe: Hãy tối thiểu Side Effect nhất có thể nhé. Có như vậy chúng ta mới có thể tránh một số bug.
Triển khai Compose & Pipe trong Javascript
Có nhiều cách để tạo ra hàm compose và pipe, mình xin giới thiệu một cách mà mình hay sử dụng. Để hiểu cách triển khai thì chúng ta xem một ví dụ thực tế:
Mình có một array các product. Bây giờ mình muốn lấy ra 3 sản phẩm, sau đó sắp xếp tăng dần theo giá tiền và cuối cùng là hiển thị các sản phẩm đó ra 😃. Chúng ta sẽ sử dụng Compose (Right to left) cho bài toán này nhé:
const data = [
{ product: 'laptop', price: 600 },
{ product: 'iphone', price: 200 },
{ product: 'fan', price: 300 },
{ product: 'keyboard', price: 150 },
{ product: 'speaker', price: 170 },
];
const compose = (f, g) => (...args) => f(g(...args));
handleData(showItems, orderByDescPrice, get3products)(data);
function handleData(...fns) {
// return fns.reduce(compose);
return fns.reduce((f, g) => {
return (data) => f(g(data));
});
}
function get3products(products) {
return newArr;
}
function orderByDescPrice(products) {
return newArr;
}
function showItems(products) {
return products;
}
Đầu tiên chúng ta sẽ triển khai hàm compose()
nhé, để hiểu được hàm này thì chúng ta xem qua hàm handleData()
. Hàm này như hàm calc()
ở ví dụ trên mình đã giải thích. Trong trường hợp có nhiều hàm và nhiều nhiệm vụ khác nhau thì chúng ta không thể cứ gọi nhiều hàm lồng nhau được. Chúng ta sẽ sử dụng method reduce
trong trường hợp này.
Trong reduce
mình truyền callback là compose
đã tạo sẵn, hoặc các bạn có thể nhìn trực tiếp method reduce mình đã để trong handleData()
để tiện theo dõi.
reduce
mình truyền 2 param. Trong đó f là hàm previous tức là hàm trước đó của hàm hiện tại và g là hàm hiện tại. Nhờ sử dụng reduce
mà chúng ta có thể thực thi các hàm theo thứ tự từ Right to Left hoặc Left to Right.
const compose = (f, g) => (...args) => f(g(...args));
Tiếp theo reduce
sẽ trả về một kết quả. Trong trường hợp này mình sẽ để nó return về một arrow function. ...args
được sử dụng khi chúng ta không biết có bao nhiêu params được truyền vào hàm.
Arrow function này sẽ return về một kết quả của biểu thức f(g(x)). Tức là g (hàm hiện tại) nhận giá trị x thực hiện nhiệm vụ đã giao và return kết quả. Kết quả này sẽ được truyền cho f (hàm trước nó) nhận kết quả này thực hiện return. Method reduce
sẽ giúp chúng ta làm công việc này.
Để sử dụng kết quả của hàm hiện tại cho hàm tiếp theo, các bạn nhớ để return giá trị của hàm hiện tại để hàm tiếp theo sử dụng nhé.
Bây giờ chúng ta hãy viết logic cho các hàm trên nhé:
function get3products(products) {
const newArr = products.filter((product, i) => {
if (i < 3) return product;
});
return newArr;
}
function orderByDescPrice(products) {
const newArr = products.sort((a, b) => b.price - a.price);
return newArr;
}
function showItems(products) {
console.log(products);
return products;
}
Các bạn có thể thấy các hàm trên là Pure function. Tức là trong các hàm này dù bạn sử dụng với mục đích gì thì cũng không được làm biến đổi bất cứ giá trị nào ở bên ngoài hàm hay phạm vi global scope. Nhờ sử dụng Pure Function mà chúng ta có thể tránh được nhiều bug.
Nếu nhận đầu vào là một array hay object thì cách tốt nhất là chúng ta hãy copy giá trị đầu vào này và sau đó modify tùy ý các bạn. Bằng cách đó chúng ta sẽ không làm biến đổi giá trị truyền vào.
Sau khi thực hiện hàm theo Compose thứ tự Right to Left:
Ta được kết quả:
// lấy ra 3 sản phẩm > sắp xếp tăng dần theo giá tiền > hiển thị các sản phẩm
0: {product: 'laptop', price: 600}
1: {product: 'fan', price: 300}
2: {product: 'iphone', price: 200}
Vậy còn Pipe thì sao? Triển khai nó thế nào? 😄 Rất đơn giản thôi nếu các bạn không quen theo thứ tự hàm Right to Left thì có thể sử dụng Pipe để thực hiện hàm theo Left to Right bằng cách:
Sử dụng g(f(x))
thay vì f(g(x)).
const compose = (f, g) => (...args) => g(f(...args));
Lời kết
Compose và Pipe là cách để chúng ta có thể reuse code. Chẳng hạn như ví dụ trên bạn muốn sau khi sắp xếp giá tiền tăng dần. Bạn muốn tìm sản phẩm có tên 'phone' thì rất đơn giản chúng ta chỉ cần viết thêm một hàm mới và đặt hàm đó vào vị trí mà bạn muốn để nó thực hiện nhiệm vụ ^^. Rất hay phải không nào.
Một số bạn khi tiếp cận thì có thể sẽ rất khó hiểu nhưng các bạn có thể xem lại vài lần hoặc lấy source về và theo dõi, console.log ở chỗ nào mà cảm thấy chưa hiểu rõ thì các bạn sẽ dần hiểu ra 😁
Thư viện về Compose và Pipe: Ramdajs
Cuối cùng xin cảm ơn các bạn đã theo dõi bài viết này. Chúc các bạn có một ngày học tập và làm việc vui vẻ. Peace 👏