Khi làm việc với Javascript, đâu đó bạn đã từng nghe nói tới khái niệm Pure Function – dịch tạm là hàm “nguyên tem”. Ví dụ, trong React Redux, các hàm thao tác với dữ liệu trong store bắt buộc phải là pure function.
Thực chất pure function không phải là một API, hay một hàm cụ thể nào, mà nó là một khái niệm về một loại hàm trong lập trình.
Vậy Pure function là gì? Ưu điểm và tại sao chúng ta lại cần tới nó?
Qua bài viết này, chúng ta cùng tìm hiểu nhé!
Nội dung chính của bài viết
Pure function là gì?
Có nhiều cách hiểu và định nghĩa khác nhau về pure function. Nhưng theo mình, chúng ta có thể hiểu đơn giản như sau:
Pure function là hàm luôn trả về cùng một kết quả khi tham số đầu vào không thay đổi, cho dù hàm đó được thực thi bao nhiêu lần đi chăng nữa.
Như vậy, giá trị mà pure function trả về chỉ phụ thuộc duy nhất vào giá trị tham số truyền vào, nó không phụ thuộc vào bất cứ trạng thái, dữ liệu bên ngoài phạm vi của hàm.
Đặc biệt là pure function không tạo ra các side-effects nào tới chương trình.
Ồ, nói tới đây lại phát sinh khái niệm mới: Side-effect là gì vậy?
Side effects
Side-effect là những thao tác được thực hiện ở bên trong một hàm mà nó ảnh hưởng tới toàn chương trình.
Ví dụ một số side effects hay gặp:
- Tạo HTTP request tới API (AJAX/fetch)
- Ghi – Cập nhật dữ liệu vào file
- Lưu dữ liệu vào Database, Local Storage
- Hiển thị dữ liệu ra màn hình
- Thay đổi nội dung của DOM
- vân vân và mây mây…
Tóm lại, side effect là những việc bạn làm nhưng không chỉ ảnh hưởng tới chính bạn, mà còn ảnh hưởng cả tới những người khác nữa.
Ví dụ minh họa
Để dễ hình dung hơn, chúng ta sẽ minh họa bằng code.
Những hàm dưới đây được coi là pure function:
// Hàm tính tổng hai số function sum(a, b) { return a + b; } console.log(sum(1, 2)) // output: 3 console.log(sum(1, 2)) // output: 3 console.log(sum(1, 2)) // output: 3
Như bạn cũng thấy, hàm sum()
này luôn trả về cùng một kết quả khi chúng ta truyền tham số vào như nhau. Cho dù bạn có gọi thực thi hàm đó bao nhiêu lần đi chăng nữa. Kết quả trả về chỉ phụ thuộc vào tham số.
Một ví dụ khác mà không phải là pure function:
let tax = Math.random(100); // Hàm tính tổng tiền sau thuế function sumAfterTax(a, b) { return ((a + b) * (tax / 100)).toFixed(0) } console.log(sumAfterTax(1000, 9000)) // output: 43 console.log(sumAfterTax(1000, 9000)) // output: 27 console.log(sumAfterTax(1000, 9000)) // output: 35
Với hàm trên, giá trị mà nó trả về ngoài việc phụ thuộc vào hai tham số mà còn phụ thuộc vào giá trị tax
từ bên ngoài. Do đó, mỗi khi hàm này thực thi, kết quả trả về có thể khác nhau, cho dù tham số truyền vào vẫn như cũ.
Một số API mà bạn gọi trong hàm có thể khiến cho một hàm không còn là pure function nữa. Ví dụ như dưới đây:
Math.random
const random = () => Math.random()
Hàm Date
const getDate = () => Date.now()
Call API
const getUsers = await fetch('/users')
Những ví dụ hàm trên được coi là không xác định vì cùng đầu vào mà đầu ra luôn khó lường, không biết trước được.
Một số lỗi khiến một hàm không còn là pure function
Về nguyên tắc, để chuyển về pure function, chúng ta nên chuyển tất cả những yếu tố ảnh hưởng tới kết quả đầu ra vào bộ tham số.
Tuy nhiên, cách chúng ta truyền tham số vào hàm cũng có thể khiến một hàm không còn là pure function nữa.
Dưới đây là một số ví dụ minh họa và cách sửa:
1. Sử dụng biến bên ngoài hàm
// Impure let min = 60; const isLessThanMin = value => value < min;
Giải pháp: sử dụng dependency injection. Sửa lại thành:
// pure const isLessThanMin = (min, value) => value > min
2. Truyền địa chỉ tham chiếu
// Impure const squares = (nums) => { for(let i = 0; i < nums.length; i++) nums[i] **= 2; }
Lý do: Liên quan tới khái niệm địa tham chiếu và tham trị. Khi truyền tham số nums
như trên là truyền địa chỉ tham chiếu và khi trong hàm sử dụng biến nums
cũng sẽ tác động tới vùng nhớ mà biến nums
trỏ tới.
Giải pháp: sử dụng hàm map()
, tạo mảng mới thay vì sửa trực tiếp.
// pure const squares = (nums) => nums.map(num => num * num)
3. Truyền tham chiếu của Object
// Impure const updateUserAge = (user, age) => { user.age = age }
Lý do: Tương tự như trường hợp trên, chúng ta sửa trực tiếp trên tham chiếu được truyền vào. Nên vô tình nó cũng ảnh hưởng tới vùng nhớ mà biến đó trỏ tới. Nên nhớ, một biến thực chất chỉ là lưu địa chỉ của vùng nhớ mà thôi.
Giải pháp: Tạo một đối tượng mới.
// Pure const updateUserAge = (user, age) => ({ ...user, age })
Lợi thế của Pure function
Có rất nhiều lý do mà chúng ta nên viết một hàm theo kiểu pure function. Nếu kể ra thì có mà cả đống.
Nhưng tóm gọn lại thì chỉ có một lý do lớn nhất: Pure function khiến cho một hàm trở thành một đơn vị logic nhỏ nhất trong ứng dụng, nó độc lập hoàn toàn với các hàm khác, độc lập với ứng dụng. Giống như tế bào trong cơ thể vậy.
Nhờ vậy, chúng ta có thể thực hiện unit test dễ dàng, chỉnh sửa nội dung hàm mà không cần quá lo ngại tới các phần khác. Chỉ cần test kỹ kết quả đầu ra nó chính xác với yêu cầu của hàm là được.
Tuy nhiên
Không phải tất cả các hàm trong chương trình đều là Pure function. Nếu không có các side effects thì làm sao ta có thể thao tác với DOM, lưu dữ liệu vào database, gọi API… Lúc này ứng dụng sẽ thành cái gì đây 😊
Quan trọng là phải kết hợp hài hòa, lúc cương lúc nhu, kết hợp sao cho hợp lý là được.
💦 Đọc thêm về Javascript:
Bình luận. Cùng nhau thảo luận nhé!