JavaScript Currying

 

Currying là gì?

Trong JavaScript, Currying là một phương pháp tổng hợp viết lại một hàm có nhiều đối số thành một chuỗi hàm, trong đó mỗi hàm chỉ nhận một đối số.

Nói cách khách nó là một kỹ thuật mà cho phép ta thay vì sử dụng một function có nhiều tham số truyền vào dài dòng thì ta có thể chuyển đổi thành những function liên tiếp có một tham số truyền vào thôi.

Cùng xem ví dụ sau:
const add = (a,b,c) => a+b+c;
const addSum = add(1,2,3);

const curry = (a) => {
  return (b) => {
    return (c) => {
      return a + b + c;
    }
  }
}
const addSumCurry = add(1,2,3);
console.log(addSum); //6
console.log(addSumCurry); //6


Như các bạn thấy nó có vẻ khá đơn giản để triển khai.
  • Nói cách khác, khi một hàm, thay vì nhận tất cả đối số cùng một lúc, lấy đối số đầu tiên và trả về một hàm mới nhận đối số thứ hai và trả về một hàm mới nhận đối số thứ ba, v.v., cho đến khi tất cả đối số đều có đã được hoàn thành.
  • Currying là một sự chuyển đổi của các hàm chuyển một hàm từ có thể gọi được là f(a, b, c) thành có thể gọi được là f(a)(b)(c). Currying không gọi một function. Nó chỉ biến đổi nó.
  • Chúng ta có thể tách hàm curry(1)(2)(3) để hiểu rõ hơn:
const curry1 = curry(1);
const curry2 = curry1(2);
const result = curry2(3);
console.log(result); // 6

Phiên bản nâng cao hơn chút xíu để hiểu về currying, chúng ta có bài toàn yêu cầu tìm tổng và tích của 2 số tự nhiên a, b. Thông thường chúng ta sẽ viết 1 hàm tính tổng kiểu sum(a,b) và multiplication(a,b) xong rồi trả kết quả đúng không? Oh no, bây giờ chúng ta đã biết dùng curying cơ mà hãy áp dụng nó ngay.
function curry(f) {
  return function(a){
    return function(b){
      return f(a, b)
    }
  }
}

function sum(a, b) {
  return a+b;
}

function multiplication(a,b) {
  return a *b;
}

let curriedSum = curry(sum);

let currieMultiplication = curry(multiplication);

console.log(currieMultiplication(10)(20)); //200

Okay chúng ta đã hiểu chút về currying rồi đúng không? Nếu chưa có thể xem thêm ví dụ dưới đây về cách triển khai currying nhé.

Giả sử có bài toán sau đây
  • Tìm các số tự nhiên bé hơn 10 mà phải là số lẻ.
  • Tìm các số tự nhiên bé hơn 20 mà phải là số chẵn.
  • Tìm các số tự nhiên bé hơn 30 mà khi đem chia cho 3 thì số dư bằng 2.
Bình thường chúng ta sẽ làm như sau đúng không?
function findNumberLess10AndOdd() {
  const result = [];
    for(let i = 0; i < 10; i++) {
      if(i % 2 !== 0) {
          result.push(i);
        }
    }
    return result;
}
console.log(findNumberLess10AndOdd());
//-->Output: [1, 3, 5, 7, 9]

function findNumberLess20AndEven() {
  const result = [];
    for(let i = 0; i < 20; i++) {
      if(i % 2 === 0) {
          result.push(i);
        }
    }
    return result;
}
console.log(findNumberLess20AndEven());
//-->Output: [0, 2, 4, 6, 8, 10, 12, 14, 16, 18]

function findNumberLess30AndDivide3surplus2() {
  const result = [];
    for(let i = 0; i < 30; i++) {
      if(i % 3 === 2) {
          result.push(i);
        }
    }
    return result;
}
console.log(findNumberLess30AndDivide3surplus2());
//-->Output: [2, 5, 8, 11, 14, 17, 20, 23, 26, 29]

Các bạn có thể thấy đoạn code trên có vẻ khá dài dòng và không có tính tái sử dụng đúng không nào?
Vì vậy hãy áp dụng currying để sửa lại đoạn code trên nhé.
const findNumberByCondition = (num) => (f) => {
  const result = [];
  for(let i = 0; i<num; i++){
    if(f(i)) {
      result.push(i);
    }
  }
  return result
}

console.log(findNumberByCondition(10)((num) => num % 2 !== 0));
//-->Output: [1, 3, 5, 7, 9]

console.log(findNumberByCondition(20)((num) => num % 2 === 0));
//-->Output: [0, 2, 4, 6, 8, 10, 12, 14, 16, 18]

console.log(findNumberByCondition(30)((num) => num % 3 === 2));
//-->Output: [2, 5, 8, 11, 14, 17, 20, 23, 26, 29]

Chúng ta đã có được đoạn code mới ngắn hơn hẳn và có vẻ dễ hiểu hơn đúng không, tính tái sử dụng của nó khá là cao ví dụ được áp dụng trong trường hợp tìm số chia hết cho 5 chẳng hạn.

Currying và Partial application

Có một vấn đề đó là các hàm phía trên của chúng ta nếu triển khai, lúc truyền attribute thiếu hoặc sai định dạng theo dạng nhiều tham số như sau thì sẽ phát sinh lỗi [Function (anonymous)]
const curry = (a) => {
  return (b) => {
    return (c) => {
      return a + b + c;
    }
  }
}
const addSumCurry = curry(1,2,3);
console.log(addSumCurry); //[Function (anonymous)]

Nếu sử dụng các thư viện như lodash thì chúng ta sẽ thấy không gặp tình trạng như trên ví dụ chúng ta có một hàm ghi log log(date, importance, message) để định dạng và truy xuất thông tin. Trong các dự án thực tế, các hàm như vậy có rất nhiều chức năng hữu ích như gửi logs, ... ở đây chúng tôi sẽ chỉ báo alert:
import _ from 'lodash';

function log(date, importance, message) {
  alert(`[${date.getHours()}:${date.getMinutes()}] [${importance}] ${message}`);
}

Currying hàm này.
log = _.curry(log);


log gọi như thường:
log(new Date(), "DEBUG", "debugging");


hoặc gọi như sau:
log((new Date())("DEBUG")("debugging"));


Bây giờ chúng ta có thể dễ dàng tạo một hàm thuận tiện cho việc tạo các logs hiện tại:
// logNow is log with fixed first argument
let logNow = log(new Date());

logNow("INFO", "message");
// [HH:mm] INFO message

logNow ở trên có thể được gọi là partially applied function hay ngắn gọn là partial.
Partial application là quá trình giảm số lượng tham số của một hàm bằng cách tạo một hàm mới với một số tham số được truyền vào.

Từ đó, chúng ta hoàn toàn có thể phát triển tiếp:
let debugNow = logNow("DEBUG");
debugNow("message");
// [HH:mm] DEBUG message

Vì thế:
  • Chúng ta không mất gì sau khi currying: log vẫn có thể gọi được như bình thường.
  • Có thể dễ dàng tạo các partial function như logNow, debugNow.

Triển khai ở mức nâng cao

Trong trường hợp bạn tổng quát hơn, cách triển khai currying nâng cao, bao quát cho các function đa đối số mà chúng ta có thể sử dụng ở trên.
const curry =(fn) =>{
    return curried = (...args) => {
        if (fn.length !== args.length){
            return curried.bind(null, ...args)
        }
    return fn(...args);
    };
}
const totalNum=(x,y,z) => {
    return x+y+z
}
const curriedTotal = curry(totalNum);
console.log(curriedTotal(10) (20) (30));



Nhận xét

Bài đăng phổ biến từ blog này

Cài đặt SSL cho website sử dụng certbot

Xây dựng một hệ thống comment real-time hoặc chat đơn giản sử dụng Pusher

CÁC BÀI TẬP SQL CƠ BẢN - PART 1

Xây dựng một hệ thống tracking hành vi người dùng (phần 1)

Xây dựng một hệ thống tracking hành vi người dùng (phần 2)

Enterprise architecture trên 1 tờ A4

Web caching (P2)

Bàn về async/await trong vòng lặp javascript

Web caching (P1)

Cài đặt môi trường để code website Rails