Javascript: Currying In JavaScript

Nội dung bài viết

Video học lập trình mỗi ngày

Currying là một khái niệm quan trọng là trung tâm của bất kỳ ngôn ngữ lập trình chức năng nào. Vì vậy, trong bài viết này chúng ta hãy xem nó là gì và tại sao nên sử dụng nó trong ngôn ngữ lập trình JavaScript.

Có bao giờ bạn đã nghe về khái niệm Currying javascript? Hoặc bao lâu rồi bạn chưa nghe lại 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. Thừa còn hơn thiếu, hiểu thêm hoặc nhớ lại thì đâu có mất mát gì???


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


Với một lập trình viên như tôi hay các bạn thì mong muốn của mỗi chúng ta có thể là ngay trong giấc mơ là viết code một cách thông minh ngắn gọn và có thể sử dụng nó nhiều lần có thể mà không lãng phí một chút thời gian nào cho việc tạo mới chức năng. Khi thực sự bạn biết và hiểu về khái niệm "Currying JavaScript" thì qủa thật là một kỹ năng tuyệt vời. Những khái niệm về currying trong javascript tôi đoán rất ít những devjs biết về nó. Do đó đẻ tiết kiệm thời gian, công sức cho những chức năng được viết lại thì chúng ta hãy nên sử dụng kỹ thuật currying trong javascript. Vậy currying trong javascript là gì? Chúng ta đi tiếp phần dưới để hiểu hơn.


Currying là gì?

Currying là một tính năng của các ngôn ngữ lập trình chức năng như Perl, Python và JavaScript. Trong bài viết này, tôi sẽ mượn JavaScript để giới thiệu cho các bạn biết cách hoạt động của một Currying như thế nào? Thật ra có thể bạn đã hiểu nó trong bài viết "Higher Order Function JavaScript" nếu thực sự bạn đã đọc nó.


Currying là một kỹ thuật đánh giá function với multiple arguments, thành một chuỗi hàm với single argument duy nhất. 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. 

Nói nhanh ở đây để chúng ta sẽ đi vào ví dụ thì hiểu trước là nó sẽ như thế này:

Vì vậy, thay vì có một chức năng trông như thế này:

fakeFunction('param1', 'param2', 'param3');

Bạn kết thúc với một cái gì đó như thế này:

fakeFunction('param1')('param2')('param3');

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 :D 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 ]

Kết Luận

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!

Bài viết được tham khảo rất nhiều nguồn 

http://www.breck-mckye.com/blog/2014/05/javascript-curry/

https://developer.mozilla.org/en-US/docs/Glossary/First-class_Function

https://www.codementor.io/michelre/currying-in-javascript-g6212s8qv

https://hughfdjackson.com/javascript/why-curry-helps/

Có thể bạn đã bị missing