Định nghĩa recursive function là gì thì chắc chúng ta đều đã biết. Tuy nhiên nếu bạn đang làm việc với một ngôn ngữ cho phép sử dụng vòng lặp như JavaScript (hay tất cả các ngôn ngữ không phải là một functional programming language), bạn sẽ hiếm khi thấy cần phải dùng đến recursive function. Đó là vì tất cả các recursive function đều có thể được viết dưới dạng một vòng lặp. Hơn nữa, trong các ngôn ngữ như vậy, hiệu năng của recursive function thường kém hơn nhiều so với vòng lặp. Dù vậy, vẫn có những trường hợp mà sử dụng recursive function sẽ dễ dàng hơn nhiều (ví dụ như khi làm việc với một cấu trúc cây) và giúp code dễ hiểu hơn, hoặc bạn muốn có một biến immutable nên không thể sử dụng vòng lặp. Ví dụ đơn giảnCũng như một recursive function luôn có thể viết được dưới dạng vòng lặp, các vòng lặp đều có thể viết lại được dưới dạng một recursive function. Lấy ví dụ một hàm với vòng lặp đơn giản thế này
Có thể được viết lại dưới dạng recursive function như sau
Trông rõ ràng là rắc rối khó hiểu hơn. Nhưng nếu bạn so sánh hàm này
Và phiên bản sử dụng vòng lặp của nó
Thì rõ ràng là dùng recursive function trong trường hợp này sẽ dễ hơn rất nhiều. Nói chung, các khái niệm toán học hoặc những thứ như như cấu trúc dữ liệu sẽ dễ dàng được thể hiện dưới dạng recursive function hơn vì chúng đều được định nghĩa dưới dạng các hàm. Viết lại chúng dưới dạng vòng lặp hẳn là sẽ tốn công sức hơn. Tuy nhiên, chắc các bạn còn nhớ hàm 2 viết dưới dạng recursive function ở trên có độ phức tạp là O(2^n). Trong khi phiên bản sử dụng vòng lặp có độ phức tạp là O(n). Như thế rõ ràng là recursive function có hiệu năng cực tệ và chúng ta trăm ngàn lần không nên dùng nó. Tất nhiên không phải vậy vì các ngôn ngữ lập trình hàm đều không có vòng lặp và chúng ta chỉ có thể sử dụng recursive function. Thế thì vì cái gì mà recursive function lại được các ngôn ngữ đó yêu quí như vậy. Không chỉ vì nó là hàm mà nó được yêu quí hơn trong các ngôn ngữ lập trình hàm mà còn vì nó có thể có hiệu năng hoàn toàn không thua kém so với vòng lặp.Tail call optimizationCùng xét một recursive function quen thuộc đó là tính giai thừa của một số
Nếu bạn gọi hàm này với một số thật to, 300000 chẳng hạn, thì browser sẽ báo lỗi
Đó là vì để chạy một function, máy tính phải nhớ vị trí hàm đó được gọi để sau khi thực hiện xong sẽ trả kết quả về vị trí đã gọi hàm đó. Đối với JavaScript, mỗi function sẽ được tạo một call stack. Để chạy một recursive function, browser sẽ phải tạo một đống call stack, và mỗi call stack không thể bị hủy cho đến khi các call stack bên trong nó được thực hiện xong. Khi có quá nhiều call stack thì browser sẽ báo lỗi như trên. Điều này cũng xảy ra với nhiều ngôn ngữ khác như Java, C++... Tuy nhiên, nếu một hàm chỉ gọi lại chính nó và không làm gì khác ở câu lệnh return, ví dụ thế này
thì call stack của nó sẽ trông như thế này
Bạn có thể thấy một đống call stack ở giữa hoàn toàn lãng phí, chỉ cần 3 trả kết quả về đoạn code ban đầu đã gọi recursive function là xong thay vì truyền qua một đống call stack như thế. Vậy nên thay vì tạo ra một đống call stack, compiler có thể sử dụng lại call stack ban đầu. Kĩ thuật này gọi là Tail call optimization. Nhờ vậy, một tail recursive function sẽ chỉ cần một stack, tương tự như một vòng lặp.Tail call optimization. Nhờ vậy, một tail recursive function sẽ chỉ cần một
stack, tương tự như một vòng lặp.ES6 đã hỗ trợ Tail call optimization trong strict mode nên hãy thử viết lại hàm 4 theo kiểu tail recursive function.
Lần này thì không bị lỗi nữa
Do it elegantlyBạn có thể thấy chúng ta thường cần đến hai hàm. Như trong ví dụ trên, hàm 5 có vai trò giống như vòng lặp trong hàm 4. Tham số accumulator chính là giá trị đầu tiên của vòng lặp. Nhờ có tính năng default argument trong ES6, ta có thể viết nó lại như thế này
It doesn't workTại thời điểm viết bài này (Chrome 54, Firefox 49, Node 7.0), nếu bạn thử chạy hàm 4 trên Node hoặc hầu hết browser trừ Safari hoặc Chrome với flag --enable-javascript-harmony thì bạn vẫn sẽ nhận được
Đó là vì để chạy một function, máy tính phải nhớ vị trí hàm đó được gọi để sau khi thực hiện xong sẽ trả kết quả về vị trí đã gọi hàm đó. Đối với JavaScript, mỗi function sẽ được tạo một call stack. Để chạy một recursive function, browser sẽ phải tạo một đống call stack, và mỗi call stack không thể bị hủy cho đến khi các call stack bên trong nó được thực hiện xong. Khi có quá nhiều call stack thì browser sẽ báo lỗi như trên. Điều này cũng xảy ra với nhiều ngôn ngữ khác như Java, C++... Tuy nhiên, nếu một hàm chỉ gọi lại chính nó và không làm gì khác ở câu lệnh return, ví dụ thế này TrampolineNếu bạn nhất định muốn dùng recursive function với JavaScript thì thật may là JavaScript là một ngôn ngữ có first-class function nên ta có thể dễ dàng sử dụng một kĩ thuật khác đó là Trampoline. Nội dung thì giống như tên của nó vậy, chúng ta sẽ gọi đi gọi lại hàm đó trong một vòng lặp. Nhờ vậy mỗi lần gọi xong, call stack của nó sẽ bị hủy và số call stack sẽ không bị vượt quá mức cho phép. Có thể viết hàm Trampoline đơn giản thế này 0Hàm factorial sẽ phải viết lại một chút 1Sử dụng trampoline vẫn sẽ không cho hiệu năng như với vòng lặp hoặc khi có tail call optimization vì chúng ta vẫn phải tạo ra rất nhiều call stack thay vì chỉ dùng 1 call stack. Ngoài ra bạn còn phải sửa lại hàm nữa, như thế có chút bất tiện. Bạn có thể xem một cách implement hàm trampoline khác mà không cần phải sửa lại hàm recursive ở đây. Lời kếtDù không phổ biến trong JavaScript do hầu hết mọi người đều quen với vòng lặp hơn, bạn vẫn sẽ gặp những trường hợp mà recursive function thật sự sẽ giúp giải quyết vấn đề dễ dàng hơn. Làm quen với recursive function sẽ giúp bạn đến gần hơn với functional programming, một mô hình lập trình khác trong JavaScript bên cạnh mô hình mà có thể bạn đã rất quen thuộc - object-oriented programming. Nếu bạn có hứng thú với functional programming, hãy thử qua Elm, một ngôn ngữ lập trình hàm được compile thành JavaScript. Còn nếu bạn đã từng làm việc với một ngôn ngữ lập trình hàm, mình tin là bạn thậm chí còn muốn dùng recursive function hơn là vòng lặp nữa. |