Có bao giờ bạn nghe về khái niệm này chưa? Hoặc bao lâu rồi dev JS chưa nghe về Currying In JavaScript. Vậy hôm nay sau bao nhiêu năm, chúng ta sẽ thảo luận khái niệm này một lần nữa.

Mình tin rằng khi đọc bài này xong, Dev JS cũng lên một trình mới.

Tại sao lại dùng Currying ?

Với một lập trình viên luôn hướng đến viết một đoạn code mà có thể sử dụng nó lại nhiều lần và ít tốn kém thời gian hơn. Curying giúp chúng ta tốt trong việc sử dụng lại các chức năng. Nói một cách khác mà trong các framework hay xây dựng các hàm helper để định nghĩa một hàm nào đó mà được sử dụng lại nhiều lần.

Vậy Currying là gì?

Currying là kỹ thuật mà cho phép chuyển đổi mt function vi nhiu tham s thành những functions liên tiếp có mt tham số. Nói cách khác, khi một function, thay vì lấy tất cả arguments cùng một lúc, lấy hàm thứ nhất và trả về hàm mới lấy hàm thứ hai và trả về hàm mới lấy hàm thứ ba, và tiếp tục cho đến khi tất cả các đối số đã được hoàn thành.

Ví dụ đối với ES5

var add =   function (a){
    return function(b){
        return function(c){
            return a+b+c;
         }        
    }
}
console.log(add(2)(3)(4)); //output 9
console.log(add(3)(4)(5)); //output 12

Dưới đây chúng ta sẽ đi sâu hơn về khái niệm này qua các ví dụ cụ thể hơn để giúp chúng ta có cái nhìn và nắm bắt cụ thể hơn.

Trong ES6 chúng ta có thể viết lại tốt hơn ở ví dụ trên :

const add = (a, b) => a + b

add(1, 2) //should return 3

Theo cách Currying thì chúng ta nên làm theo cách này :

const add = a => b => a + b

add(1)(2) //should return 3

Chúng ta để ý một chút ở function add. Đây là một function lấy một đối số và trả về một hàm khác nhận đối số thứ hai. Một khi tất cả các đối số ở đó, tính toán xảy ra.

Vì vậy, trong ví dụ này, gọi add (1) trả về một hàm. Curry có thể có trong JavaScript vì các hàm là first-class(https://developer.mozilla.org/en-US/docs/Glossary/First-class_Function). Điều đó có nghĩa là gì? Nó có nghĩa là các chức năng giống như bất kỳ giá trị khác. Chúng có thể được gán cho các biến, được truyền dưới dạng đối số cho các hàm khác và được trả về bởi các hàm.

Đến đây tôi cá rằng các bạn vẫn chưa hình dung được sự quang trong của Currying đúng không? Ok bây giờ chúng ta sẽ nói về tầm quan trọng của Currying trong việc trọng tâm của các function.

Tại sao Currying lại quan trọng

Currying cung cấp cho bạn cơ hội để cấu hình một phần chức năng và sau đó, nó là phương tiện để tạo các chức năng có thể sử dụng lại. Hãy lấy một ví dụ khác. Giả sử tôi có collection như thế này:

const movies = [
  {
    "id": 1,
    "name": "Matrix"
  },
  {
    "id": 2,
    "name": "Star Wars"
  },
  {
    "id": 3,
    "name": "The wolf of Wall Street"
  }
]

Bây giờ tôi muốn xuất id của các bộ phim thì tôi làm cách này, còn nhiều cách khác nhưng tôi viết theo cách tôi hay dùng. Đừng ý kiến cò gì đây nha 😀 So ez:

movies.map((movie) => movie.id) 

//should return [ 1, 2, 3 ]

Điều đó ok với mọi người. Nhưng chuẩn bị có biến nè, tôi có collection thứ 2

const series = [
  {
    "id": 4,
    "name": "South Park"
  },
  {
    "id": 5,
    "name": "The Simpsons"
  },
  {
    "id": 6,
    "name": "The Big Bang Theory"
  }
]

Và tôi cũng muốn lấy id từ trong series array này thì tôi cũng phải lặp lại một lần nữa? Xin nhấn mạnh rằng, lặp lại một lần nữa.

series.map((serie) => serie.id) //should return [ 4, 5, 6 ]

Bạn thấy có phiền không? Khi một callback trong map() lại được lặp lại một cách giống nhau. Có thể bạn bình thường nhưng với tôi điều đó là quá tệ cho sự lặp lại đó. Vậy Currying đã giúp chúng ta như thế nào trong trường hợp nay. Chúng ta hãy thực hiện một lệnh gọi hàm nên trích xuất một thuộc tính từ một đối tượng: Dùng Currying ES6:

const get = property => object => object[property];

ES5 const get = function (pro){ return function (object){ return object[pro] } } Bây giờ từ hàm này get() tôi có thể tạo một hàm khác, được gọi là getId, đó chỉ là một cấu hình một phần của hàm get:

const getId = get('id');

Và tiếp theo getId vẫn là một chức năng và điều này thật tuyệt vời vì giờ đây chúng ta có thể sử dụng nó trong các map() ở hai collection trên

movies.map(getId); //should return [ 1, 2, 3 ]
series.map(getId); //should return [ 4, 5, 6 ]

Điều đó không tốt sao? Nhưng chúng ta có thể tiến thêm một bước. Giả sử bây giờ, chúng tôi muốn trích xuất tên từ các đối tượng của chúng tôi. Làm thế nào bạn có thể đạt được điều đó? Chỉ cần tạo một hàm gọi là getName có nguồn gốc từ hàm get:

const getName = get('name')

Và làm tương tự như vậy chúng ta sẽ lấy được các thuộc tính name

movies.map(getName); //should return [ 'Matrix', 'Star Wars', 'The wolf of Wall Street' ]

Wow quá ngon, vì đây là ví dụ đơn giản để bạn thấy mà nó đã quá cool rồi thì các hàm phức tạp thì như thế nào? Các bạn cần thêm ví dụ nào nữa không, nếu không thì chúng ta sẽ đi vào phần tiếp theo, à jquery cũng đa số sử dụng Currying á nha. ví dụ như addClass thì các bạn hình dung nó như thế nào? Nó sẽ là thế này.

function addClass(cssClass, element) {
    $(element).addClass(cssClass);
}

Code full:

const movies = [
  {
    "id": 1,
    "name": "Matrix"
  },
  {
    "id": 2,
    "name": "Star Wars"
  },
  {
    "id": 3,
    "name": "The wolf of Wall Street"
  }
]

const series = [
  {
    "id": 4,
    "name": "South Park"
  },
  {
    "id": 5,
    "name": "The Simpsons"
  },
  {
    "id": 6,
    "name": "The Big Bang Theory"
  }
]

//Không tốt
console.log(series.map((serie) => serie.id)) //should return [ 1, 2, 3 ])

console.log(movies.map((movie) => movie.id)) //should return [ 1, 2, 3 ])

//Tốt 
const get = property => object => object[property];

const getId = get('id'); // if need get name then get('name')

console.log(movies.map(getId)); //should return [ 1, 2, 3 ]
console.log(series.map(getId)); //should return [ 4, 5, 6 ]

Currying là một phần quan trọng của một functional programing. Tôi khuyến khích bạn sử dụng với nó như tôi đã làm trong bài viết này. À nếu bạn sử dụng các thư viện chức năng như Ramda hoặc Lodash / fp, hầu hết các chức năng đều được curryfied theo mặc định, điều này thật tuyệt!

Vì vậy, hãy nhớ rằng phân tách các hàm của n đối số thành n hàm của 1 đối số không phải là một nhiệm vụ thực sự khó khăn với cú pháp ES6 và các hàm lamda! Vì vậy, bạn không có lý do gì để không tạo mã có thể tái sử dụng nhờ vào Currying!