- vừa được xem lúc

[JavaScript] Bài 24 - Procedural & Functional

0 0 6

Người đăng: Semi Art

Theo Viblo Asia

+-------------------------------------------+
| Reflective & Reactive |
| Data-Driven & Event-Driven |
| Object-Oriented & Agent-Oriented |
| Procedural & Functional |
| Imperative & Declarative |
+-------------------------------------------+

Trong bài viết này, chúng ta sẽ cùng tản mạn về Procedural ProgrammingFunctional Programming - tạm dịch là lập trình thủ tụclập trình hàm.

Hàm thì chúng ta biết rồi nhưng thủ tục là cái gì thế?

Trước khi bắt đầu, bạn có thể đặt khái niệm hàm mà chúng ta đã biết sang một bên được không? 😄 Bởi vì để thuận lợi cho quãng thời gian khởi đầu, mình đã cố gắng giới thiệu các khái niệm theo hướng dễ tiếp cận nhất. Nhưng tới thời điểm hiện tại thì những cách hiểu cũ của chúng ta không hẳn là hoàn toàn phù hợp nữa. Ở đây chúng ta sẽ lại xuất phát từ vị trí hơi gần con số 0 nhé. 😄

Về cơ bản thì các thủ tục và các hàm toán học khi được biểu thị trong các ngôn ngữ lập trình phổ biến sẽ đều có điểm chung là các khối lệnh được đặt tên và có thể gọi được - hay callable.

Điểm khác biệt chính giữa hai khái niệm này là một thủ tục hay procedure, được xem là một tác vụ, hay một công việc cần được tiến hành, hay một hành động của một chủ thể nào đó tác động lên một tối tượng dữ liệu để tạo ra sự thay đổi, cập nhật trên đối tượng dữ liệu đó.

Trong khi đó thì một hàm toán học hay function, lại không được xem là một tác vụ, hay công việc, hay hành động của một chủ thể nào và không tác động lên một đối tượng dữ liệu nào cả. Một hàm (toán học) chỉ đơn giản là một định nghĩa biểu thị mối liên hệ tương quan giữa các yếu tố thường được gọi là các tham số và một giá trị đích đến.

Trong một số ngôn ngữ lập trình như Ada hay SQL (Server) thì việc khai báo các thủ tục và các hàm sẽ được phân biệt bởi từ khóa procedurefunction. Điều này giúp cho người sử dụng luôn phân biệt được rất rõ hai khái niệm này và giúp cho việc thiết kế các khối lệnh có khả năng tái sử dụng sẽ trở nên có chủ đích rõ ràng và rành mạch hơn.

Còn ở đây, với JavaScript, chúng ta có một từ khóa được tạo ra bởi một lỗi đánh máy và được sử dụng chung cho cả hai. 😄

// thủ tục thực hiện công việc
// tăng giá trị của một object lên gấp hai lần function doubleIt(theObject) { theObject.value *= 2
} var just = { value: 1 };
doubleIt(just);
console.log(just); // { value: 2 }
// hàm f(x) = x * 2 function f(x) { return x * 2;
} let one = 1;
f(1);
console.log(one); // 1 // tạo ra một giá trị mới
let two = f(1);
console.log(two); // 2

Nếu vậy khi nói tới hàm có nghĩa là chúng ta chỉ làm việc với các giá trị số học?

Không, hoàn toàn không phải vậy. Khái niệm hàm đúng là được vay mượn từ toán học, nhưng trong lập trình nói chung thì hoàn toàn không hề bị giới hạn xung quanh các định nghĩa liên quan tới các giá trị số học. Khi sử dụng hàm, chúng ta chỉ cần đảm bảo tiêu chí ban đầu - đó là không thực hiện thao tác nào tác động thay đổi lên các đối tượng dữ liệu. Tất cả những gì chúng ta làm là định nghĩa mối liên hệ tương quan giữa tham số của hàm và một giá trị đích đến.

// hàm f(object) đối chiếu giữa một object ban đầu
// với một object đích đến có giá trị gấp hai lần function doubleOf(theObject) { let anotherValue = theObject.value * 2; let targetObject = { value: anotherValue }; return targetObject
} let just = { value: 1 };
let anotherJust = doubleOf(just); console.log(just); // { value: 1 }
console.log(anotherJust); // { value: 2 }

Và như đã nói, trong code ví dụ ở trên, chúng ta đã không thực hiện thao tác nào tác động lên just và không tạo ra thay đổi nào cả. Hàm doubleOf chỉ đơn giản là chỉ ra mối liên hệ giữa tham số theObjecttargetObject. Các hàm hoàn toàn không quan tâm tới câu hỏi Việc cần làm là gì?; Và cũng không quan tâm tới các yếu tố của môi trường bên ngoài phần định nghĩa hàm.

Khi chúng ta sử dụng hàm doubleOf và cung cấp một object just bất kỳ vào vị trí của tham số theObject, thì logic định nghĩa bởi doubleOf sẽ chỉ đường cho chúng ta tìm đến một object khác anotherJust. Theo cách nói của các bạn yêu thích môn toán thì đó là một ánh xạ từ miền giá trị này sang một miền giá trị khác. Mấy từ ánh xạ với miền giá trị nghe oách thật; Nhưng mà thôi, chúng ta cứ dùng từ chỉ đường đi cho dân dã. 😄

Những đặc tính cơ bản của Procedural ProgrammingFunctional Programming

Xuất phát từ những đặc tính cơ bản ở trên thì chúng ta có thêm được một số cái gạch đầu dòng về các đặc tính cơ bản của proceduralfunctional. Tuy nhiên thì để dễ nhớ hơn, chúng ta sẽ liệt kê những đặc tính này ở dạng so sánh song song giữa hai khía cạnh tư duy này.

a. Imperative & Declarative

Với những gì mà chúng ta đã thảo luận từ nãy tới giờ thì rõ ràng là chúng ta có thể nhận ra đặc điểm của procedural được đặt nền móng trên imperative mà chúng ta đã nói đến ở bài viết trước. Chính vì vậy nên hai khái niệm Procedural ProgramingImperative Programming đôi khi được người ta hiểu là một. Tuy nhiên thì cái tên imperative chỉ nói chung chung về khía cạnh tuần tự của code liên ứng với logic vận hành của chương trình, còn từ procedural lại được sử dụng để nhấn vào trọng tâm thiết kế phần mềm là các khối code đại diện cho các tác vụ nhỏ hay phương thức hành động của chương trình.

void function main() { var a = { value: null }; var b = { value: null }; var result = { value: null }; getUserInput(a, b); calculate(a, b, result); updateView(result);
} (); // chạy chương trình function getUserInput(out_A, out_B) { var inputA = document.getElementById('input-a'); out_A.value = Number.parseInt(inputA.value); var inputB = document.getElementById('input-b'); out_B.value = Number.parseInt(inputB.value);
} // getUserInput function calculate(in_A, in_B, out_Result) { out_Result.value = (in_A.value + in_B.value) * 1001;
} // calculate function updateView(in_Value) { var view = document.getElementById('result'); view.textContent = in_Value;
} // updateView

Trong khi đó thì functional ở khía cạnh khác lại được đặt nền móng trên declarative, và nhấn vào trọng tâm thiết kế phần mềm là các khối code biểu thị các mối liên kết giữa các nút giá trị trong chương trình. Chẳng hạn khi người dùng thao tác và tạo ra một sự kiện, chúng ta nhận được một giá trị A và trong nội dung của code sẽ chỉ toàn là các hàm biểu thị liên hệ từ A trỏ tới B rồi tới C ... rồi tới Z. Giá trị A ban đầu sẽ không bị thay đổi, và giá trị Z tìm thấy ở đâu đó sau khi đi theo chỉ dẫn của các hàm sẽ được sử dụng để phản hồi cho người dùng.

Nói tới đây thì chúng ta cũng thấy rằng functionaldeclarative về cơ bản sẽ không thể tách rời hoàn toàn khỏi imperative. Bởi vì sau cùng thì phần mềm mà chúng ta viết ra vẫn sẽ phải phản hồi lại kết quả cho môi trường bên ngoài theo cách nào đó. Các định nghĩa của functionaldeclarative về cơ bản là các giá trị trừu tượng thụ động và sẽ được sử dụng bởi môi trường bên ngoài.

Chính vì vậy nên ngay cả các ngôn ngữ được gọi là thuần Functional Programming ví dụ như Haskell vẫn có một chút code Input/Output được viết ở dạng imperative. Và nếu như ứng vào ví dụ ở phía trên thì chúng ta có các thủ tục getUserInputupdateView thuộc về các tác vụ Input/Output hiển nhiên không thể thay thế bởi code functional. Vị trí của functional là ở giai đoạn đi từ các giá trị input tới result, và có thể thay thế cho thủ tục calculate.

b. Phương Thức & GIá Trị

Các thủ tục hay procedure về cơ bản thì như chúng ta đã nói đó là các hành động của một chủ thể nào đó. Ngay cả khi chương trình mà chúng ta viết ra không làm việc với các object thì chúng ta vẫn có thể xem đó là các phương thức của phần mềm tổng bộ. Một thủ tục có ý nghĩa biểu thị là một thao tác hay cách thức thực hiện công việc; Và không có ý nghĩa biểu thị là một giá trị. Các thủ tục thường thực hiện tác động thay đổi các đối tượng dữ liệu - có thể là thay đổi giá trị của các biến ngoại vi của môi trường bên ngoài hoặc thay đổi nội dung của đối tượng dữ liệu được truyền vào.

Trong khi đó, các hàm hay function như chúng ta cũng vừa thảo luận thì lại không biểu thị cho hành động hay cách thức thực hiện công việc, và sẽ không thực hiện tác động thay đổi lên các đối tượng dữ liệu. Và bởi vì ứng với mỗi một giá trị ban đầu, chúng ta luôn luôn có thể sử dụng một hàm để đối chiếu tới một giá trị khác ở đâu đó; Do đó nên một hàm còn được xem là biểu thị cho một giá trị trừu tượng, và chúng ta có thể truyền các giá trị trừu tượng kiểu này vào một hàm nào đó khác cần sử dụng - hoặc trả một giá trị trừu tượng ở vị trí một hàm được gọi. 😄

const map = function(func) { return function(arr) { var [first, ...rest] = arr; if (arr.length == 0) return []; if ('normal-case') return [func(first), ...map(func)(rest)]; };
}; // map const doubleOf = function(num) { return num * 2;
}; const tripleOf = function(num) { return num * 3;
}; let arr = [1, 2, 3, 4, 5, 6, 7, 8, 9]; let doubleOfArr = map(doubleOf)(arr);
console.log(doubleOfArr);
// [2, 4, 6, 8, 10, 12, 14, 16,18] let tripleOfArr = map(tripleOf)(arr);
console.log(tripleOfArr);
// [3, 6, 9, 12, 15, 18, 21, 24, 27]

Bạn thấy đấy, chính vì đặc điểm một hàm có thể được xem là một giá trị. Chúng ta có thể kết hợp các hàm với nhau để tạo ra một logic hoạt động rất linh hoạt. Hàm map trong ví dụ ở trên chỉ biểu thị liên hệ giữa func, arr, và kết quả đích đến là một mảng mới nào đó. Tuy nhiên logic dẫn đường từ arr tới mảng kết quả sẽ còn phụ thuộc vào việc chúng ta truyền hàm nào vào vị trí của func. 😄

Bên cạnh đó, nếu như hướng tư duy procedural quan tâm rất nhiều tới việc biểu thị phương thức hay thao tác xử lý, thì functional ở khía cạnh khác lại đặc biệt quan tâm tới việc biểu thị các miền giá trị. Chúng ta có thể truyền một miền giá trị vô hạn vào một lời gọi hàm để được chỉ dẫn tới một miền giá trị đích cũng có độ rộng vô hạn.

Tuy nhiên các ngôn ngữ chủ điểm hỗ trợ functional sẽ có sẵn một tính năng tên là Lazy Evaluation - tạm dịch là chế độ tính toán trễ - để trì hoãn việc thực hiện tính toán ngay tại thời điểm gọi hàm với một tập giá trị vô hạn như vậy. Và chỉ khi chúng ta cần lấy ra một dải giá trị hữu hạn từ tập kết quả thì tiến trình tính toán mới thực sự được thực hiện. Còn trong JavaScript thì chúng ta sẽ cần nhờ tới function* và tự xây dựng hàm truy xuất các khoảng giá trị con và cách viết code triển khai có phần mang hơi hướng imperative. 😄

const range = function (min) { return function* (max) { while (min <= max) { yield min; min += 1; } } // return
}; // range const take = function (n) { return function (range) { let first = range.next().value; if (n == 0) return []; if ('normal-case') return [first, ...take(n-1)(range)]; }
}; // take let positiveInt = range(1)(Infinity);
let oneToNine = take(9)(positiveInt);
console.log(oneToNine);
// [1, 2, 3, 4, 5, 6, 7, 8, 9]

Trên thực tế thì việc viết định nghĩa để mô tả và sử dụng các miền giá trị vô hạn như trên cần thêm thao tác thiết lập lại generator mỗi khi take. Tuy nhiên thì ở đây chúng ta chỉ tạm tập trung vào minh họa khái niệm Lazy Evaluation để hiểu hơn về lối tư duy functional thôi. 😄

c. Trạng Thái & Bất Biến

Chính bởi vì vị trí đặc trưng của procedural là tiếp giáp tới những nơi lưu dữ liệu tương tác hay trạng thái state; Kết quả hoạt động của một thủ tục thường sẽ phụ thuộc vào những yếu tố khác bên ngoài.

Chúng ta có thể thực hiện nhiều lần truy vấn tới cùng một thành phần input của một giao diện web và nhận được kết quả mỗi lần mỗi khác, tùy vào tương tác của người dùng. Chúng ta cũng có thể gửi nhiều yêu cầu truy vấn tới cơ sở dữ liệu và nhận được kết quả mỗi lần mỗi khác tùy vào những cập nhật xảy ra trong cơ sở dữ liệu.

Trong khi đó thì các lời gọi một hàm với cùng một dữ kiện đầu vào, sẽ luôn luôn trỏ tới chính xác một kết quả đích đến. Với một giá trị A cụ thể ban đầu, sau một lộ trình di chuyển qua các nút giá trị, chắc chắn chúng ta sẽ chỉ tìm thấy một giá trị Z duy nhất, kết quả này sẽ luôn đúng với 1001 lần vận hành code functional. Điều này sẽ giúp chúng ta duy trì được kết quả hoạt động của code dễ phỏng đoán, và việc kiểm tra hay sửa lỗi logic cũng sẽ rất thuận lợi. 😄

Do đó nên khi muốn áp dụng lối tư duy functional trong JavaScript, chúng ta sẽ luôn luôn cần cố gắng không chạm vào các thao tác thay đổi giá trị của bất kỳ biến nào xuất hiện trong code. Và trong cả việc lựa chọn các phương thức làm việc với các nút dữ liệu cũng cần tránh sử dụng những phương thức can thiệp vào nội dung của các đối tượng dữ liệu. Nói ngắn gọn hơn là chúng ta cần đảm bảo các giá trị đều bất biến immutable.

let arr = [1, 2, 3, 4, 5, 6, 7, 8, 9]; console.log('=== tạo ra mảng mới từ mảng arr và các phần tử muốn bổ sung');
let paddedArr = [0, ...arr, 10]; console.log(arr);
// [1, 2, 3, 4, 5, 6, 7, 8, 9]
console.log(paddedArr);
// [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10] console.log('=== tạo ra mảng mới từ paddedArr bớt đi phần tử đầu tiên');
let trimmedLeft = paddedArr.slice(1); console.log(paddedArr);
// [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
console.log(trimmedLeft);
// [1, 2, 3, 4, 5, 6, 7, 8, 9, 10] console.log('=== tạo ra mảng mới từ paddedArr bớt đi phần tử cuối cùng');
let trimmedRight = paddedArr.slice(0, -1); console.log(paddedArr);
// [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
console.log(trimmedRight);
// [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]

d. Nối Tiếp & Kết Hợp

Để chuyển tiếp kết quả hoạt động từ một thủ tục tới một thủ tục khác, chúng ta không cần làm thao tác gì đặc biệt cả. Bởi vì các thủ tục đều là các thao tác khách quan tác động lên các đối tượng dữ liệu. Do đó chúng ta chỉ cần cung cấp các địa chỉ tham chiếu cho các thủ tục để tìm tới và xử lý dữ liệu giống như trong ví dụ mà chúng ta đã có trước đó với các lời gọi nối tiếp chaining.

void function main() { var a = { value: null }; var b = { value: null }; var result = { value: null }; getUserInput(a, b); calculate(a, b, result); updateView(result);
} (); // chạy chương trình ...

Ở đây chúng ta thấy các thủ tục getUserInput, calculate, và updateView sẽ lần lượt tìm tới các đối tượng dữ liệu a, b, và result để thao tác đọc hoặc chỉnh sửa giá trị.

Trong khi đó, để chuyển tiếp kết quả hoạt động giữa các hàm thì chúng ta có thể biểu thị sự kết hợp composition các chặng đường thành một lộ trình đầu -> cuối rồi sau đó thực hiện gọi hàm bằng reduce.

const add_1 = function(x) { return x + 1;
}; const multiply_2 = function(x) { return x * 2;
}; const subtract_3 = function(x) { return x - 3;
}; const power_4 = function(x) { return x ** 4;
}; let one = 1; // xuất phát từ 1 let pipe = [ add_1, // đi tới 2 power_4, // đi tới 16 multiply_2, // đi tới 32 subtract_3, // đi tới 29
]; let target = pipe.reduce((x, f) => f(x), one); console.log(one); // 1
console.log(target); // 29

Ở các ngôn ngữ chủ điểm hỗ trợ functional người ta còn cung cấp thêm cách viết biểu thị sự kết hợp của các hàm theo dạng biểu thức thông thường. Tuy nhiên trong JavaScript thì chúng ta có thể sử dụng cách viết như trên để theo dõi tuần tự của code từ trên xuống cũng được.

fn = add_1 . power_4 . multiply_2 . subtract_3
fn 1
-- 29

Một số đặc tính chung khác

Ngoài những đặc tính đã nêu trên thì các ngôn ngữ lập trình hiện đại đều cố gắng hỗ trợ các tính năng chung rất phổ biến để đáp ứng với nhu cầu xây dựng những phần mềm có tính năng đa dạng. Những đặc tính này có thể kể tên là - Trừu Tượng Abstraction, Đóng Gói Encapsulation, Kế Thừa Inheritance, và Đa Hình Polymorphism.

Đây là các đặc tính chung trong thiết kế phần mềm chứ không bị giới hạn ở của riêng ngôn ngữ hay mô hình lập tình nào cả. Tuy nhiên do bài viết này tới đây đã hơi quá dài nên chúng ta sẽ tạm không quan tâm tới việc thể hiện chúng trong PP (Procedural Programming) hay FP (Functional Programming) như thế nào.

Các thuật ngữ này rất phổ biến trong OOP (Object-Oriented Programming) và nhiều khi được người ta hiểu nhầm thành đặc tính riêng của OOP. Và tiện thể khi nói tới OOP ở bài viết sau thì chúng ta sẽ nói về chúng. Khi chúng ta hiểu cách mà những đặc tính này được biểu thị trên nền móng OOP thì chúng ta cũng sẽ hiểu cách để có thể mang chúng tới FP hay bất kỳ đâu mà chúng ta cần, với tất cả những khả năng mà một ngôn ngữ hay một môi trường vận hành cung cấp.

Kết thúc bài viết

Bài viết giới thiệu về hai khía cạnh tư duy Procedural & Functional của chúng ta đến đây là kết thúc. Trong bài viết tiếp theo, chúng ta sẽ cùng tản mạn về Object-Oriented ProgrammingAgent-Oriented Programming. Còn bây giờ thì đã đến lúc nghỉ giải lao rồi. Hẹn gặp lại bạn sau.

(Sắp đăng tải) [JavaScript] Bài 25 - Object-Oriented & Agent-Oriented

Bình luận

Bài viết tương tự

- vừa được xem lúc

Giới thiệu Typescript - Sự khác nhau giữa Typescript và Javascript

Typescript là gì. TypeScript là một ngôn ngữ giúp cung cấp quy mô lớn hơn so với JavaScript.

0 0 500

- vừa được xem lúc

Bạn đã biết các tips này khi làm việc với chuỗi trong JavaScript chưa ?

Hi xin chào các bạn, tiếp tục chuỗi chủ đề về cái thằng JavaScript này, hôm nay mình sẽ giới thiệu cho các bạn một số thủ thuật hay ho khi làm việc với chuỗi trong JavaScript có thể bạn đã hoặc chưa từng dùng. Cụ thể như nào thì hãy cùng mình tìm hiểu trong bài viết này nhé (go).

0 0 414

- vừa được xem lúc

Một số phương thức với object trong Javascript

Trong Javascript có hỗ trợ các loại dữ liệu cơ bản là giống với hầu hết những ngôn ngữ lập trình khác. Bài viết này mình sẽ giới thiệu về Object và một số phương thức thường dùng với nó.

0 0 136

- vừa được xem lúc

Tìm hiểu về thư viện axios

Giới thiệu. Axios là gì? Axios là một thư viện HTTP Client dựa trên Promise.

0 0 117

- vừa được xem lúc

Imports và Exports trong JavaScript ES6

. Giới thiệu. ES6 cung cấp cho chúng ta import (nhập), export (xuất) các functions, biến từ module này sang module khác và sử dụng nó trong các file khác.

0 0 93

- vừa được xem lúc

Bài toán đọc số thành chữ (phần 2) - Hoàn chỉnh chương trình dưới 100 dòng code

Tiếp tục bài viết còn dang dở ở phần trước Phân tích bài toán đọc số thành chữ (phần 1) - Phân tích đề và những mảnh ghép đầu tiên. Bạn nào chưa đọc thì có thể xem ở link trên trước nhé.

0 0 229