Triển khai mã hiệu quả hơn với compose & pipe function trong Javascript

  • 0

  • 0

  • 0

Triển khai mã hiệu quả hơn với compose & pipe function trong Javascript

Triển khai mã hiệu quả hơn với compose & pipe function trong Javascript

Giới thiệu về composition function

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.

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é.

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 addmult 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.

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 composepipe 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 composepipe 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ạn thấy bài viết này có ích?
  • Không

  • Bình thường

Bình luận
DMCA.com Protection Status