Nhóm nội dung cuối cùng trong Sub-Series JavaScript
của chúng ta là thảo luận về các mô hình lập trình paradigm
phổ biến - hay có thể hiểu nôm na là các phương thức tư duy tổng quan khi lập trình phần mềm. Nếu như bạn thử Google với từ khóa programming paradigms
thì chắc chắn là bạn sẽ được giới thiệu tới liên kết gần nhất là Wikipedia với một danh sách các paradigm
rất dài. Tuy nhiên chúng ta sẽ chỉ quan tâm tới một vài mô hình lập trình phổ biến nhất được liệt kê dưới đây -
+-------------------------------------------+
| Reflective & Reactive |
| Data-Driven & Event-Driven |
| Object-Oriented & Agent-Oriented |
| Procedural & Functional |
| Imperative & Declarative |
+-------------------------------------------+
Và ở đây, trong bài viết này, chúng ta sẽ khởi đầu với 2 mô hình lập trình có tên là Imperative Programming
và Declarative Programming
- tạm dịch là tư duy lập trình tuần tự
và tư duy lập trình định nghĩa
. Trên thực tế thì nếu như bạn Google Translate từ imperative
sẽ có ý nghĩa là mệnh lệnh
, còn declarative
sẽ được dịch là khai báo
. Bạn có thể tạm giữ một chút băn khoăn về phần dịch nghĩa này và tự chọn lựa cái nào phù hợp với cách hiểu của bạn sau khi chúng ta thực hiện xong bài viết này.
Tại sao chúng ta nên tìm hiểu về các mô hình lập trình?
Bởi vì việc vông việc lập trình nói chung về cơ bản là truyền tải tiến trình logic trong tâm trí của chúng ta trở thành một giải pháp phần mềm trong máy tính; Do đó yếu tố căn bản nhất và có ảnh hưởng đến tiến trình tạo ra phần mềm của chúng ta nhiều nhất - chính là phương thức tư duy mà chúng ta lựa chọn để truyền tải ý tưởng thành phần mềm. Nếu như chúng ta có thể nhìn một vấn đề theo nhiều khía cạnh khác nhau, thì điều đó có nghĩa là chúng ta sẽ có nhiều lựa chọn hơn ở mỗi thời điểm.
Hiển nhiên là sẽ không có thứ gì thực sự là tốt nhất trong mọi tình huống. Tuy nhiên ứng với mỗi trường hợp cụ thể, chúng ta sẽ có thể chọn ra được những cách thức phù hợp nhất để truyền tải ý tưởng của mình thành phần mềm.
Khả năng thay đổi tư duy và cách thức nhìn nhận vấn đề thực sự đáng giá hơn rất nhiều so với điểm số IQ.
_Alan Kay
Và hơn thế nữa, mỗi một mô hình lập trình cũng giống như mỗi một khái niệm khác mà chúng ta đã được biết - đều được gắn liền với cuộc sống thường nhật của chúng ta. Bởi tất cả chỉ là một - cách thức mà logic trong tâm trí của chúng ta được thể hiện vào trong code cũng cũng là những dạng thức pattern
mà tâm trí tự nó đưa ra những nhận định, phân tích về môi trường sống xung quanh. Đâu đó, có lẽ việc học các mô hình lập trình cũng sẽ giúp cho chúng ta hiểu thêm được phần nào về cách thức mà ơn trên sắp xếp và xoay vần dòng chảy của cuộc sống.
Nếu vậy có nghĩa là chúng ta đã và đang sử dụng các mô hình này từ trước đến giờ mà không biết?
Đúng là như vậy. Mỗi một mô hình lập trình, nếu như được hiểu theo một cách khác thì chỉ đơn giản là một khía cạnh tư duy đặt nền móng tư duy để viết ra một đoạn code hoặc ở bình diện lớn hơn thì là để cấu trúc nên một phần mềm. Việc tìm hiểu các mô hình lập trình về cơ bản không phải là học những kiến thức mới, mà là nhận diện và đặt tên cho những thứ mà chúng ta đã biết; Và sau đó chúng ta sẽ có thể nhìn nhận một tác vụ cần thực hiện trong phần mềm một cách rành mạch hơn. Bây giờ thì chúng ta hãy bắt đầu thôi.
Tuần Tự & Định Nghĩa
Ở đây mình vẫn ghi chú lại lần nữa tên nguyên bản để bạn Google tìm hiểu các thông tin liên quan nếu cần thiết nhé. Tên của hai mô hình lập trình đầu tiên mà chúng ta nói đến là Imperative Programming
và Declarative Programming
.
Imperative Programming
có thể hiểu nôm na là khi chúng ta nhìn nhận một chương trình là một tuần tự các câu lệnh chỉ dẫn cho máy tính việc cần làm là gì? - hay trình tự các bước để thực hiện một công việc là như thế nào?
Trong khi đó Declarative Programming
ở một khía cạnh khác lại là câu hỏi - chúng ta cần phải định nghĩa hay giải thích cho máy tính hiểu - mục tiêu hay kết quả mà chúng ta đang hướng đến là cái gì?
Chúng ta sẽ thử lấy một ví dụ đơn giản về một hàm sumArray
tính tổng một mảng số nguyên bất kỳ. Và trước tiên sẽ là lối tư duy tuần tự imperative
-
var numberArray = [1, 2, 3, 4, 5, 6, 7, 8, 9]; function sumArray(arr) { var total = 0; for (var num of arr) { total += num; } return total;
}; // sumArray console.log( sumArray(numberArray) );
// 45
Trong code ví dụ ở trên thì chúng ta đã chỉ ra cho máy tính tuần tự công việc được thực hiện như sau -
- Khởi tạo một cái hộp rỗng
total
giả định là tổng xuất phát ban đầu là0
. - Sau đó tìm tới mảng
arr
để lấy ra phần tử đầu tiên và cộng vàototal
- Sau đó lấy tiếp ra phần tử thứ hai, thứ ba... và cộng gộp dần vào
total
- Tới khi cộng gộp lần lượt các phần tử vào
total
xong thì có nghĩa là chúng ta đã có tổng cần tính.
Và code ví dụ dưới đây là ở một khía cạnh tư duy khác - declarative
, khi chúng ta suy nghĩ về việc giải thích cho máy tính hiểu tổng mà chúng ta cần tính là cái gì?
-
var numberArray = [1, 2, 3, 4, 5, 6, 7, 8, 9]; const sumArray = function(arr) { var [first, ...rest] = arr if (arr.length == 0) return 0; if ('normal-case') return first + sumArray(rest);
}; console.log( sumArray(numberArray) );
// 45
Tổng sumArr
của một mảng arr
có thể hiểu đơn giản là - giá trị của phần tử đầu tiên first
đem cộng gộp với tổng sumArr
của mảng con chứa tất cả các phần tử còn lại rest
. Định nghĩa này hiển nhiên là chúng ta thấy rất rõ ràng và dễ hiểu.
Tuy nhiên nó chưa đầy đủ và phù hợp cho mọi tình huống. Cụ thể là khi mảng arr
không có phần tử nào thì chúng ta sẽ không thể nói như vậy được. Do đó chúng ta cần phải bổ sung vào định nghĩa này thêm một chút để đảm bảo trong trường hợp nào thì máy tính cũng sẽ không gặp khó.
Tổng sumArr
của một mảng arr
còn có thể là 0
trong trường hợp mảng arr
không có phần tử nào cả.
Trích đoạn bài viết [JavaScript] Bài 11 - Hàm & Vùng -
[ Máy tính ] "Tổng của mảng [1, 2, 3, 4, 5, 6, 7, 8, 9] là gì?"
[Người dùng] "Ủa, tưởng giỏi tính toán hơn tôi. Sao hỏi kỳ thế?"
[ Máy tính ] "Tôi giỏi tính toán chứ không giỏi tiếng Việt. Tổng đấy là cái gì thế?"
[Người dùng] "Ừ thì là: 1 + tổng của mảng [2, 3, 4, 5, 6, 7, 8, 9]"
[ Máy tính ] "Ok, đó là bước 1. Cái đoạn `1 +` thì tôi biết rồi. Nhưng tổng của mảng còn lại là cái gì?"
[Người dùng] "Ừ thì là: 2 + tổng của mảng [3, 4, 5, 6, 7, 8, 9]"
[ Máy tính ] "Ok, đó là bước 2. Cái đoạn `2 +` thì tôi biết rồi. Nhưng tổng của mảng còn lại là cái gì?"
[Người dùng] "Ừ thì là: 3 + tổng của mảng [4, 5, 6, 7, 8, 9]"
. . .
. . .
[ Máy tính ] "Ok, đó là bước 8. Cái đoạn `8 +` thì tôi biết rồi. Nhưng tổng của mảng còn lại là cái gì?"
[Người dùng] "Ừ thì là: 9 + tổng của mảng []"
[ Máy tính ] "Ok, đó là bước 9. Cái đoạn `9 +` thì tôi biết rồi. Nhưng tổng của mảng [] là cái gì?"
[Người dùng] "Là 0. Có gì đâu để mà tính. Thế ra kết quả chưa?" [ Máy tính ] "Từ từ để tôi quay lại bước 9 đã... Tổng là 9."
[Người dùng] "Tôi đang hỏi cái tổng của mảng ban đầu. =,="
[ Máy tính ] "Từ từ để tôi quay lại bước 8 đã... Tổng là 17."
[Người dùng] "Tôi đang hỏi cái tổng của mảng ban đầu. =,="
. . .
. . .
[ Máy tính ] "Từ từ để tôi quay lại bước 2 đã... Tổng là ..."
[Người dùng] "Tôi đang hỏi cái tổng của mảng ban đầu. =,="
[ Máy tính ] "Từ từ để tôi quay lại bước 1 đã... Tổng là 45. Kết quả cuối cùng rồi đấy." [Người dùng] "Ok, thế bây giờ tính giúp tôi tổng của mảng [1, 2, 3, ..., 100_000] được không?"
[ Máy tính ] "Tôi chỉ nhớ được khoảng 10_000 bước thôi."
Một chút suy nghĩ
Hai mô hình lập trình đầu tiên mà chúng ta tìm kiếm tên gọi để nhận diện ở đây cũng là hai khía cạnh tư duy cơ bản, tương ứng với 2 dạng câu hỏi thường nhật mà tâm trí của chúng ta phải đáp ứng thường ngày, với mỗi tác vụ công việc cần thực hiện - Như thế nào?
và Là cái gì?
.
Thực tế thì câu hỏi Là cái gì?
luôn luôn là câu hỏi tới trước, bởi nó biểu thị khi chúng ta đang đứng ở vị trí quan sát bên ngoài công việc cần thực hiện. Tuy nhiên trải nghiệm cuộc sống của chúng ta lại trực tiếp nghiệm thu trong tiến trình thực hiện công việc, do đó nên câu hỏi Như thế nào?
dường như được chú ý nhiều hơn cả.
Đây là lý do các ngôn ngữ lập trình phổ biến đều hỗ trợ Imperative Programming
trước hết, với các từ khóa mang ý nghĩa giải thích tuần tự logic cần thực hiện, và cả những công cụ khác nữa. Mặc dù với mục đích là truyền đạt lối biểu thị declarative
vào code JavaScript, chúng ta vẫn không thể viết như thế này -
sumArr [] is 0
sumArr [first, ...rest] is first + sumArr(rest)
Tuy nhiên thì điều đó có lẽ là không quan trọng lắm. Điều tuyệt vời ở đây là chúng ta là người Việt và ý nghĩa của một từ khóa tiếng Anh nhiều khi sẽ là do chúng ta quy ước. Đối với mỗi từ khóa if
trong code declarative
ở phía trên, bạn cứ xem như đó là một ký hiệu giúp tránh phải viết lặp lại tên hàm sumArr
cũng được.
Vậy JavaScript có hỗ trợ declarative
?
Chắc chắn là có. Như đã nói ở trên thì declarative
là một khía cạnh tư duy căn bản luôn đứng ngay sát cạnh imperative
. Một đặc điểm điển hình của declarative
đó là lối viết diễn dịch
- là khi chúng ta đưa ra định nghĩa trừu tượng hay sử dụng một cái tên nào đó trước, rồi mới thể hiện chi tiết diễn giải về cái tên đó ở phía sau. JavaScript cũng như nhiều ngôn ngữ khác có cho phép chúng ta gọi và sử dụng một hàm đứng trước phần code định nghĩa để tạo ra hàm đó.
Ở các ngôn ngữ có nền móng chủ điểm là declarative
thì chúng ta sẽ có thêm các cú pháp binding
để giải thích các yếu tố trong định nghĩa được viết ở dòng trên. Việc này cũng có thể thực hiện được trong JavaScript bằng cách sử dụng các hàm tự gọi, nhưng làm như vậy cũng không hẳn cần thiết và sẽ khiến code của chúng ta khó đọc hơn.
-- thể tích của một chiếc hộp có các cạnh a, b, c
-- là diện tích của mặt chứa cạnh a, b nhân với c
boxVolume a b c = area * c -- giải thích về diện tích area trong dòng trên where area = a * b
Một vài công cụ imperative
chưa nhắc đến
Do trọng tâm của JavaScript là tuần tự của các câu lệnh cần thực hiện, các ngôn ngữ lập trình phổ biến có cung cấp thêm các công cụ giúp chúng ta thay đổi trình tự thực hiện của các câu lệnh khi cần thiết. Điều này nhằm mục đích để tạo ra logic hoạt động đa dạng hơn cho code và có nhiều khả năng để cho chúng ta chuyển tải các ý tưởng thành phần mềm.
Xuyên suốt Sub-Series JavaScript
của chúng ta ở đây thì chỉ có duy nhất từ khóa break
trong cấu trúc điều kiện switch
mà chúng ta đã biết được thiết kế với mục đích như vậy. Chúng ta cũng có thể sử dụng break
để rời khởi một vòng lặp for
hay while
trước khi chu trình lặp kết thúc. Và như vậy là tuần tự của công việc đang thực hiện được thay đổi rất đột ngột.
Bên cạnh đó thì JavaScript
còn 2 công cụ nữa là từ khóa continue
và label
. Trong đó thì continue
sẽ giúp chúng ta break
nhẹ nhẹ qua một lần lặp và vẫn tiếp tục thực hiện chu trình lặp sau đó; Còn label
thì có thể giúp chúng ta tạo ra các nhãn đặt tên cho các khối lệnh để break
có thể xác định phạm vi và thoát ra ngoài khối lệnh đó cho dù có bao nhiêu khối lệnh xếp chồng bên trong cũng không quan trọng.
Lý do mà mình không đưa các công cụ này vào phần nội dung chính của Sub-Series
là vì các công cụ này thực sự không phải là bắt buộc hay quá cần thiết. Trong khi đó thì JavaScript cung cấp cho chúng ta rất nhiều phương thức khác để thể hiện công việc cần thực hiện với logic của code dễ theo dõi hơn. Bởi vì dù gì ở khía cạnh imperative
, điều mà chúng ta mong muốn nhất vẫn là code có tuần tự dễ theo dõi và phỏng đoán.
Trong bài viết tiếp theo, chúng ta sẽ thảo luận về hai mô hình lập trình Procedural & Functional
. À không.. từ bây giờ chúng ta không dùng từ mô hình lập trình
được không? Chúng ta chỉ gọi là những khía cạnh tư duy thôi, như vậy nghe đỡ bác học
hơn mà thân thiện hơn.
Vậy đấy, những bài viết cuối cùng trong Sub-Series JavaScript
của chúng ta về cơ bản chỉ là những câu chuyện tản mạn. Vì toàn là những thứ mà chúng ta đã biết rồi mà.
(Sắp đăng tải) [JavaScript] Bài 24 - Procedural & Functional