Functional Programming - Phần 3 - Buông bỏ Javascript 248 nodejs 72 Functional Programming 10 prog
Phần 1: Con đường sáng Phần 2: Nhập đạo Phần 3: Buông bỏ Functional Programming là một con đường khác, một phương pháp tư duy khác trong coding. Ở tầm nhìn trừu tượng hơn, người ta xếp Functional Programming vào nhóm "Declarative", còn OOP thuộc nhóm "Imperative". Từ các bài học ngữ ...
- Phần 1: Con đường sáng
- Phần 2: Nhập đạo
- Phần 3: Buông bỏ
Functional Programming là một con đường khác, một phương pháp tư duy khác trong coding. Ở tầm nhìn trừu tượng hơn, người ta xếp Functional Programming vào nhóm "Declarative", còn OOP thuộc nhóm "Imperative".
Từ các bài học ngữ pháp chúng ta đã biết 2 kiểu câu: câu trần thuật (Declarative Sentence), và câu mệnh lệnh (Imperative Sentence).
Lập trình theo lối "Imperative Programming" là sắp xếp một loạt các mệnh lệnh liên tiếp, để máy tính thực thi tuần tự từng bước. Ở đây người ta tập trung vào "how". Nào, hãy làm thế này, rồi làm thế kia... Một hình thức "cầm tay chỉ việc".
Ví dụ trên trang web có 4 boxes màu đỏ thế này:
<style> .box { awidth: 100px; height: 100px; float: left; margin: 10px; background-color: red; } .hide { display: none; } </style> <div class="box hide"></div> <div class="box hide"></div> <div class="box hide"></div> <div class="box hide"></div>
Mấy boxes này đang ẩn, ta cần làm chúng hiện ra bằng cách loại bỏ class "hide" đi.
Các giáo viên tin học đáng kính ở trường xưa thường dạy viết kiểu như thế này:
// tìm hết các tags có class "box': const els = document.querySelectorAll('.box'); // quét tất cả các tags tìm thấy for (let i = 0; i < els.length; i++) { // với tag thứ i let el = els[i]; // xóa bỏ class "hide" để cho tag hiện lên el.classList.remove('hide'); // nếu còn phần tử phía sau thì tăng i lên 1 đơn vị // quay lại với xử lý tag thứ i + 1 }
Họ dùng code hướng dẫn cho máy tính làm từng nhiệm vụ.
Người tu luyện Functional Programming không tư duy theo cách đó.
No for/while
"Declarative Programming" là tập trung vào "what". Chúng ta chỉ cần định nghĩa những quy tắc đầu vào, đầu ra. Chẳng hạn "nếu input là 1 thì output là 2". Phần còn lại để máy tính xử lý.
Người tu luyện Functional Programming không cần for loop.
Code như thế này nhìn mới mẻ hơn nhiều:
const getElements = (selector) => { return Array.from(document.querySelectorAll(selector)); }; const getRemover = (el) => { return (className) => { el.classList.remove(className); return el; }; }; const els = getElements('.box') .map(getRemover) .map(removeClass => removeClass('hide')); console.log(els);
Trước tiên chúng ta tạo ra 1 pure function getElements dùng để lấy các elements trên trang thông qua CSS Selector. Tập hợp này vốn là ArrayLike, ta dùng Array.from chuyển thành Array thực sự để có thể tận dụng các phương thức trong Array prototype.
Ở đây ta định nghĩa input là CSS Selector, output là 1 mảng DOM Elements.
Còn getRemover lại là 1 higher-order function. Có thể gọi nó bằng cách chaining getRemover(DOMElement)(classToRemove). Chúng ta lợi dụng đặc tính của higher-order function, sau 2 lần map thì chạm tới function do getRemover ném lại.
Ở đây ta định nghĩa input là DOM Element, output là function() {nhận input là className và output là DOM Element đã mất đi class đó}.
Code như vậy ta có thể đem logic dùng lại ở nhiều chỗ khác nhau, chỉ cần thay đổi input. Ví dụ loại bỏ class float-left khỏi tất cả các thẻ div.
const els = getElements('div') .map(getRemover) .map(removeClass => removeClass('float-left'));
Khi đã thấm nhuần tư tưởng Functional Programming, và không quan tâm đến việc log ra các elements, ta có thể viết lại script trên một cách ngắn gọn như thế này:
const map = fn => arr => arr.map(fn); const getElements = selector => Array.from(document.querySelectorAll(selector)); const removeClass = className => el => el.classList.remove(className); const removeClassHide = removeClass('hide'); const getListBoxElement = getElements('.box'); map(removeClassHide)(getListBoxElement);
No if/else
Người tu luyện Functional Programming cũng không cần if/else.
Thậm chí họ còn tạo ra cả một chiến dịch Anti-IF!
Có nhiều cách để loại bỏ hoàn toàn if/else ra khỏi chương trình của bạn. Đơn giản nhất là dùng ternary.
Ternary
Trong JavaScript, ternary - tam phân - có tên gọi chính thức là Toán tử Điều kiện - Conditional Operator. Nó là cách viết ngắn gọn của if/else.
Hãy xem đoạn code dài dòng, rẽ nhánh phức tạp như sau:
let title = 'Mr.'; if (person.gender === 'female') { if (!person.gotMarried) { title = 'Ms.'; } else { title = 'Mrs.'; } }
Có thể được viết gọn lại thành:
const title = person.gender === 'female' ? (!person.gotMarried ? 'Ms.' : 'Mrs.') : 'Mr.';
Không còn if/else nữa.
Ta cũng vô hình trung loại bỏ được var, let vì không cần gán lại giá trị cho title.
Logical operators
Cách thứ 2 là khai thác sức mạnh ngầm của các logical operators &&, ||. Đây là những toán tử logic. Hôm trước có bạn viết 1 cái TIL ngắn khá hay. Sau đây ta quan sát chúng kỹ hơn qua lăng kính Functional Programming.
Giả sử có đoạn code như sau:
const sayHello = () => { console.log('Hello, bonjour, nihao'); return true; }; const doNothing = () => { console.log('Do nothing'); return false; }; const greet = (hasClient) => { if (hasClient) { sayHello(); } else { doNothing(); } } greet(true); // => 'Hello, bonjour, nihao' greet(false); //=> 'Do nothing'
Về mặt logic, hàm greet() kiểm tra điều kiện nếu có khách thì chào, nếu không thì không làm gì cả.
Theo định nghĩa của && và ||, chúng ta biết:
- expr1 && expr2 trả về expr1 nếu expr1 là falsy, ngoài ra nó trả về expr2.
Một điều thú vị ở đây là JavaScript engine luôn ước lượng giá trị biểu thức logic dạng này từ trái sang phải và theo nguyên tắc "đoản mạch" - short-circuit evaluation". Tiên hữu nào giỏi Vật lý chắc còn nhớ hiện tượng "đoản mạch", đó là khi dòng điện không chạy qua tải hoặc chỉ chạy qua một phần.
Vì AND chỉ trả về true nếu cả 2 mệnh đề cùng đúng, nên ngay khi bắt gặp expr1 sai, nó lập tức kết luận mệnh đề ghép là Sai và chấm dứt tại đó luôn, không chạy qua nửa bên phải expr2 nữa.
- expr1 || expr2 trả về expr1 nếu expr1 là truthy, ngoài ra nó trả về expr2.
Vì OR trả về true nếu ít nhất 1 mệnh đề đúng, nên ngay khi bắt gặp expr1 đúng, nó lập tức kết luận mệnh đề ghép là Đúng và bỏ qua expr2.
Short-circuit thần thánh!
Các lập trình viên kinh nghiệm thường lợi dụng đặc điểm này để tối ưu hiệu suất chương trình. Họ để các biểu thức tính toán phức tạp ở nửa sau của biểu thức logic. Như vậy, khi chưa rơi vào hoàn cảnh thích hợp, chúng sẽ bị bỏ qua, không cần tốn resource xử lý.
Tới đây, ta đã có thể viết lại hàm greet() một cách bí hiểm như sau:
const greet = (hasClient) => { return (hasClient || doNothing()) && sayHello(); }
Bắt đầu phần nằm trong ngoặc đơn bên trái &&. Nếu hasClient là true thì giá trị phần này cũng là true, doNothing() bị bỏ qua.
Vì phần bên trái của && là true nên cuối cùng, giá trị biểu thức quy về phần bên phải &&, tức là sayHello().
Lập luận tương tự cho trường hợp hasClient là false, dòng chảy chương trình lập tức rẽ sang doNothing(). Lúc này giá trị nửa bên trái && là false, do đó không cần quan tâm đến sayHello() nữa.
Viết như trên vừa độc vừa lạ, vừa khử được if/else, mà vẫn hoàn toàn ăn khớp với điều kiện quy ước.
Tuy nhiên, logical operators nếu nhìn không quen thì có vẻ hơi khó hình dung mạch suy diễn của chương trình. Tôi chỉ đưa ra đây để các tin hữu tham khảo. Trong dự án thực tế, vẫn nên dùng ternary cho đỡ hại não đồng đội:
const greet = (hasClient) => { return hasClient ? sayHello() : doNothing(); }
Logical functions
Một cách tiếp cận khác thể hiện tinh thần Functional Programming quyết liệt hơn, đó là tạo ra các hàm đặc trách nhiệm vụ xử lý logic. Ví dụ trong Ramda.js và Sanctuary đều có ifElse , unless , when, và hàng chục hàm logic khác.
Hàm greet nếu viết lại với Ramda sẽ trở nên xinh xắn như thế này:
const R = require('ramda'); const greet = R.ifElse(R.identity, sayHello, doNothing);
Đó là vẻ đẹp đầy tính nghệ thuật của Function Composition. Bạn cứ ngắm nhìn nó và đừng nói gì cả! Composition cũng có nghĩa là tác phẩm, như thơ của Paul Verlaine hay nhạc của Beethoven.
No new/this
Có 2 thứ luôn khiến Brendan Eich cảm thấy hài lòng khi kể về lịch sử JavaScript, đó là first-class function và prototype mechanism.
Ngày nay, hầu hết developer đều biết rằng thừa kế trong JavaScript là prototype-based inheritance. Nhưng ở thời kỳ web còn hoang sơ, người ta hay dùng new và các hàm constructors để lập trình OOP trong JavaScript theo kiểu class-based, giống như bên Java vẫn làm.
Classical inheritance
Cổ thư ghi lại rất nhiều ví dụ kiểu này:
function Dog(name) { this.name = name; this.say = function() { console.log('woof-woof, my name is ' + this.name); } } var rocky = new Dog('Rocky'); rocky.say(); var molly = new Dog('Molly'); molly.say();
Hàm Dog gọi là Function Constructor, các tiền bối chân giới Đại Việt thủa trước chuyển ngữ thành "hàm dựng". Còn chúng ta thời nay có lẽ cứ nên giữ nguyên văn.
Prototypal inheritance
Sang đầu kỷ thứ 3, ở tông môn Yahoo! có một vị trưởng lão tu vi rất cao thâm tên là Douglas Crockford tung ra bộ kỳ thư "JavaScript: The Good Parts", trong đó có đoạn nhấn mạnh bản chất prototype trong JavaScript, sự khác biệt giữa classical inheritance và prototypal inheritance. Ông cho rằng từ khóa new mang theo nhiều điểm bất cập, nên khuyến khích dùng Object.create để sao chép nguyên mẫu sang đối tượng kế thừa.
Tư tưởng của Douglas Crockford quả thực mới mẻ. Vào lúc đó, nhiều JavaScript engine còn chưa kịp hỗ trợ Object.create. Cuốn này vừa ra mắt đã gây náo loạn cả tin giới, trở thành sách gối đầu giường của rất nhiều tu sĩ.
Object.create cho phép sao chép các properties hoặc protoype của đối tượng. Hàm Dog có thể được viết lại theo hướng prototypal inheritance như thế này:
function Dog() {} Dog.prototype.say = function() { console.log('woof-woof, my name is ' + this.name); } var rocky = Object.create(Dog.prototype); rocky.name = 'Rocky'; var molly = Object.create(Dog.prototype); molly.name = 'Molly'; rocky.say(); molly.say();
Không cần new nữa!
Các cường giả sau đó nhanh chóng phát triển thêm nhiều cách tiếp cận prototypal inheritance khác, nổi bật nhất phải kể đến Concatenative inheritance, Prototype delegation và Functional inheritance.
ES6 Class ngày nay chỉ vay mượn syntax của classical OOP để làm interface, còn bên trong nó chính là cơ chế prototypal inheritance.
Object Composition
Nhưng dù sao prototypal inheritance vẫn thuộc về OOP.
Người tu luyện Functional Programming không cần new.
Gần 10 năm sau bom tấn "The Good Parts", Douglas Crockford lại một lần nữa khiến tin giới chấn động bằng "JavaScript: The Better Parts". Thời điểm này, ông đã không còn dùng Object.create() nữa, cũng từ bỏ luôn this, for loops, for in, while... Tu vi của ông đã tiến thêm một bước lớn. Trong clip, ông nói về những tính năng mới của ES6 lúc ấy vẫn còn chưa chính thức xuất xưởng. Mấy lão quái kiệt này luôn đi trước thiên hạ vài năm.
Đó cũng là khi trào lưu Functional Programming đang dần nóng trở lại, người ta bắt đầu nhắc đến khái niệm Object Composition.
Đi cặp với new là this. Từ khóa this chẳng qua chỉ là kỹ xảo nhằm tạo ra một ngữ cảnh khu biệt (context) để thực thi các hàm. Trong JavaScript, mỗi hàm như 1 kết giới độc lập. Function khi được gắn lên object thì gọi là method. Ngữ cảnh method đó chạy thường chính là đối tượng sở hữu nó. Sau này mới sinh ra các thủ thuật bind, apply, call để đánh tráo context.
Với những người mới học JavaScript, this đôi khi trở thành nỗi khiếp sợ. Rất khó debug các vấn đề phát sinh trong hàm nếu không biết chính xác ngữ cảnh chạy nó. Mà ngữ cảnh lại thường không ổn định. Đúng hơn, phải nói rằng chúng luôn luôn mutable.
Người tu luyện Functional Programming không cần this.
Đoạn code với classical OOP trên kia có thể viết lại thành:
const sayName = (state) => { return Object.assign( state, { say: () => { console.log(`woof-woof, my name is ${state.name}`); }, } ); }; const createDog = (name) => { let state = { name, }; return Object.assign(state, sayName(state)); }; const rocky = createDog('Rocky'); rocky.say(); const molly = createDog('Molly'); molly.say();
Nhìn đâu cũng thấy functions.
Không còn for/while, if/else, new/this.
Liệu bạn đã sẵn sàng rời khỏi những phàm vật ấy?
Hay nói như các nhà sư, liệu bạn có thể buông bỏ?
Khi lối tư duy truyền thống đã ăn sâu vào tâm trí, hễ gặp vấn đề phân cấp đối tượng thì chúng ta sẽ nghĩ ngay đến OOP, class, prototype, inheritance... thậm chí coi chúng như giải pháp tất yếu, duy nhất. Hễ xử lý tập hợp là phải looping, hễ thấy có điều kiện thì chỉ biết dựa vào if... Đây là trở ngại rất lớn cho kẻ mới nhập đạo.
Phải tìm cách rũ bỏ những thứ không cần thiết, thì mới đi xa được.
Rời khỏi chúng, chỉ giữ lại một ý niệm duy nhất: FUNCTION!
Nhất niệm "phân sần"!
Ban đầu tất nhiên là sẽ khó khăn, lúng túng. Giống như hàng ngày bạn vẫn đi trên con đường quen thuộc từ nhà đến công sở rồi lại trở về nhà. Cho đến một hôm con đường đó bị cảnh sát chặn lại, bạn đành phải rẽ sang lối khác.
Trên con đường xa lạ ấy, bạn không còn trông thấy những điểm mốc hàng ngày vẫn thấy: 1 shop lưu niệm, 1 cây xăng, 1 tiệm cầm đồ, sau ngã tư là đến ven sông, cây cầu sơn màu đỏ, một tiệm tạp hóa thường có cô em rất xinh ngồi trước cửa... Bạn không còn bắt gặp những dấu hiệu đã quen mắt. Bạn chẳng biết mình đã đi đến đâu, còn cách nhà bao nhiêu km nữa.
Nhưng con đường nào đi lại vài lần thì cũng thành quen. Chẳng có gì đáng ngại. Vấn đề là, ngay khi bạn nhận thấy Functional Programming là thứ gì đó rất thú vị, đáng để học hỏi, vận dụng nó, bạn nên thực hiện ngay lập tức, đừng chờ đợi dịp nào thuận tiện, đừng chờ tìm được minh sư dẫn dắt. Nếu vậy, bạn sẽ khó mà rời khỏi lối mòn xưa cũ.
Krishnamurti từng diễn giải một điều gần tương tự, đại ý thế này:
Nếu bạn đi về hướng Bắc suốt những ngày tháng của cuộc đời bạn, giống như con người đã đi theo một hướng đặc biệt, rồi có người nào đó xuất hiện và nói, “Hướng đó không đúng”. Sau đó ông ta bảo bạn, “Đi về hướng Nam, hướng Đông, bất kỳ hướng nào, ngoại trừ hướng đó.” Và khi bạn thực sự chuyển động khỏi hướng đó, có một sự thay đổi ngay tại chính những tế bào não bởi vì bạn đã phá vỡ cái khuôn mẫu. Và cái khuôn mẫu đó phải được phá vỡ ngay lúc này, không phải bốn mươi năm hay một trăm năm sau.
`
- Xem phần 1: Con đường sáng
- Xem phần 2: Nhập đạo