Dạo này nghiên cứu javascript hơi nhiều biết thêm những tính năng hay ho trong ES6 nên viết bài blog chia sẻ về này. Bài viết này sẽ nói về Pure và Impure trong javascript và làm một số ví dụ để hiểu rõ hơn về nó.
Pure và Impure functions
Pure functions: là những hàm thuần khiết không phụ thuộc và không làm thay đổi các trạng thái của các biến bên ngoài phạm vi của nó. Điều đó có nghĩa là hàm luôn trả ra một kết quả giống như tham số truyền vào là thực thi độc lập.
Mình mượn tạm các ví dụ trên stackoverflow: đây là ví dụ về impure function trước khi nói đến pure function
var values = { a: 1 }; function impureFunction ( items ) { var b = 1; items.a = items.a * b + 2; return items.a; } var c = impureFunction( values );
Hàm impureFunction bên trên truyền vào một object và modify thuộc tính của object truyền vào. vì vậy nó là impure function.
var values = { a: 1 }; function pureFunction ( a ) { var b = 1; a = a * b + 2; return a; } var c = pureFunction( values.a );
Ở ví dụ này. hàm pureFunction là thuần khiết vì nó nhận vào 1 tham số là kiểu nguyên thủy (primitive) và không làm thay đổi giá trị nó bên ngoài phạm vi hàm nên nó là pure function. Đến ví dụ tiếp theo.
var values = { a: 1 }; var b = 1; function impureFunction ( a ) { a = a * b + 2; return a; } var c = impureFunction( values.a );
Hàm này không thuần khiết vì nó sự dụng biến b bên ngoài hàm.
var values = { a: 1 }; var b = 1; function pureFunction ( a, c ) { a = a * c + 2; return a; } var c = pureFunction( values.a, b );
và đây là các giải quyết cho hàm bên trên để nó thật sự thuần khiết =))
Lợi ích của nó mang lại là độc lập, không làm thay đổi cấu trúc của tham số truyền vào dễ dàng refactor, tổ chức lại code, làm cho ứng dụng của bạn linh hoạt hơn. Xem thêm về Pure Function tại đây. Không nói năng linh tinh nữa, chúng ta sẽ đi thẳng vào một vài ví dụ để hiểu rõ hơn về nó.
Practice
Mình đã có chuẩn bị sẵn một project trên Github. Các bạn clone về và làm hoặc không thích có thể checkout qua các nhánh để xem chi tiết các ví dụ.
Cấu trúc project rất đơn giản: bao gồm thư mục js chứa main.js. file index.html, và các file config.
Các package mình đã sử dụng trong project như:
- babel: trình biên dịch javascript từ ES6 sáng ES5 để có thể chạy trên các trình duyệt cũ.
- http-server: một package để chạy server local
- webpack: Tạo bundle cho javascript.
- deep-freeze: Một package ngăn sự thay đổi thuộc tính của object
- expect: Package hỗ trợ cho test
- concurrently: (optional) Hỗ trợ run nhiều script trong 1 command npm
Trước khi bắt đầu luyện tập cần chuẩn bị một số thứ như restore các package bằng lệnh npm install. Nếu thư mục gốc của project chưa có thư mục build thì bạn phải tạo nó và chạy lệnh npm start. Mở trình duyệt với url http://localhost:9000. Có vẻ mọi thứ đã ổn. Chúng ta sẽ bắt đầu với ví dụ đầu tiên. Bắt đầu với js/main.js.
import expect from 'expect' var deepFreeze = require('deep-freeze'); const addCounter = (list, number) => { //Impure function list.push(number); return list; } const testAddCounter = () => { const listBefore = []; const listAfter = [10]; //add deepFreeze here expect( addCounter(listBefore, 10) ).toEqual(listAfter); } testAddCounter(); console.log("Test passed!!!");
Ví dụ đầu tiên thêm số vào một mảng bằng hàm addCounter sử dụng arrow function trong ES6. Mọi thứ có vẻ đơn giản chỉ việc nhận vào một cái list và số cần thêm vào, sau đó push nó vào list. Và thông qua hàm testAddCounter để kiểm tra tính đúng đắn của hàm. Mọi thứ rất OK. Để đảm bảo là tham số truyền vào không bị modify bởi hàm addCounter chúng ta thêm deepFreeze(listBefore) vào trước expect() và save lại. Refresh lại trang và xem console.
deepFreeze đã ngăn việc thêm phần tử vào listBefore và thông báo lỗi trên. điều đó có nghĩa là list truyền vào addCounter đã không còn pure. OK fine, Chúng ta sẽ thay đổi hàm addCounter một chút để đảm cho hàm được “pure”.
const addCounter = (list, number) => { //Impure function // list.push(number); // return list; return list.concat(number); }
Chỉ đơn giản bằng việc thay đổi hàm push thành concat() để hàm được “pure”, save lại và kiểm tra console (Checkout nhánh sample-1 để xem full code).
import expect from 'expect' var deepFreeze = require('deep-freeze'); const incrementCounter = (list, index) => { //impure function list[index]++; return list; } const testIncrementCounter = () => { const listBefore = [10, 20, 30]; const listAfter = [10, 21, 30]; //deepFreeze expect( incrementCounter(listBefore, 1) ).toEqual(listAfter); } testIncrementCounter(); console.log("Test passed");
Ví dụ thứ 2 cũng đơn giản bằng việc tăng thêm 1 ở vị trí index thông qua hàm incrementCounter. Test case vẫn passed cho đến khi thêm vào deepFreeze(listBefore) bên trên expect()
Chúng ta sẽ nhận được thông báo lỗi sau khi đã save và refresh trang. Điều đó chứng tỏ là hàm incrementCounter() là impure function. Thay thế hàm incrementCounter bằng đoạn bên dưới.
const incrementCounter = (list, index) => { return list .slice(0, index) .concat(list[index]+1) .concat(list.slice(index+1)); }
Bằng cách sử dụng slice() để lấy ra các phần tử từ vị trí từ 0 đến index sau đó sử dụng concat() join với phần tử ở vị trí index tăng lên 1 và cuối cùng join với các phần từ phía sau index (index + 1). Chúng ta sẽ sử dụng một tính năng mới của ES6 đó là Spread Syntax để implement chỗ này.
const incrementCounter = (list, index) => { return [ ...list.slice(0, index), list[index] + 1, ...list.slice(index + 1) ]; }
import expect from 'expect' var deepFreeze = require('deep-freeze'); const todos = (listTodo, action) => { switch (action.type) { case 'ADD_TODO': return [ ...listTodo, { id: action.id, name: action.name, completed: false } ]; case 'TOGGLE_TODO': return listTodo.map(todo => { if (todo.id !== action.id){ return todo; } return { ...todo, completed: !todo.completed } }) default: return listTodo; } } const testAddTodo = () => { const todoBefore = []; const action = { type: 'ADD_TODO', id: 0, name: 'Write blog' } const todoAfter = [ { id: 0, name: 'Write blog', completed: false } ] deepFreeze(todoBefore); deepFreeze(action); expect( todos(todoBefore, action) ).toEqual(todoAfter); } const testToggleTodo = () => { const todoBefore = [ { id:0, name: 'Write blog', completed: false }, { id:1, name: 'Drink beer', completed: false }, ]; const action = { type: 'TOGGLE_TODO', id: 1 }; const todoAfter = [ { id:0, name: 'Write blog', completed: false }, { id:1, name: 'Drink beer', completed: true }, ]; deepFreeze(todoBefore); deepFreeze(action); expect( todos(todoBefore, action) ).toEqual(todoAfter); } testAddTodo(); testToggleTodo(); console.log("All test passed!!");
Đến ví dụ này có chút phức tạp 🙂 với 1 hàm todos() truyền vào 2 tham số là listToDo và action. Nếu action có type là “ADD_TODO” thì sẽ thêm một object vào listToDo. Nếu action.type là ‘TOGGLE_TODO” thì chuyển trạng thái của todo có id bằng action.id thành completed: true. Nhớ thêm default trong switch case nếu action.type không phù hợp với bất kì type nào để tránh bug =)). Bằng việc sử dụng deepFreeze để đảm bảo listToDo và action truyền vào không bị modify bởi hàm todos(). Trong các hàm test giá trị ban đầu nằm ở todoBefore và bằng cách thực hiện các action để có kết quả mong đợi nằm ở todoAfter. Mọi thứ đều passed bằng việc sử dụng spread operator với object. Để làm được điều đó đừng quên thêm vào config trong babel với dòng sau.
"plugins": ["transform-object-rest-spread"]
Thông qua 3 ví dụ bên trên có thể bạn đã hiểu rõ về pure function và các hiện thực nó trong mỗi dòng code.
OK. Bài viết đã kết thúc. tiếp theo list to do là “DRINK BEER” :))