lập trình hàm
Photo: serokell.io

Lập trình hàm (function programming) đã có trong ngành phát triển phần mềm từ những ngày đầu tiên, nhưng ngày càng có tầm quan trọng trong kỷ nguyên hiện đại. Bài viết này xem xét các khái niệm đằng sau lập trình hàm và cung cấp hiểu biết thực tế với các ví dụ trong JavaScript và Java.

Lập trình hàm là gì?

Các hàm là đơn vị cơ bản để tổ chức code; chúng tồn tại trong tất cả các ngôn ngữ lập trình bậc cao. Nói chung, lập trình hàm có nghĩa là sử dụng các hàm để có hiệu quả tốt nhất trong việc tạo phần mềm sạch và có thể bảo trì. Cụ thể hơn, lập trình hàm là một tập hợp các phương pháp tiếp cận để coding, thường được mô tả như một mô hình lập trình.

Lập trình hàm đôi khi được định nghĩa đối lập với lập trình hướng đối tượng (OOP) và lập trình thủ tục (procedure programming). Điều đó là sai lầm vì những cách tiếp cận này không loại trừ lẫn nhau và hầu hết các hệ thống có xu hướng sử dụng cả ba.

Lập trình hàm mang lại những lợi ích rõ ràng trong một số trường hợp nhất định, nó được sử dụng nhiều trong nhiều ngôn ngữ và farmework, đồng thời nổi bật trong xu hướng phần mềm hiện tại. Nó là một công cụ hữu ích và mạnh mẽ nên là một phần của bộ công cụ khái niệm và cú pháp của mọi developers.

lập trình hàm
Photo: guru99.com

Các hàm thuần khiết (Pure functions)

Lý tưởng trong lập trình hàm là những gì được gọi là các hàm thuần khiết. Một hàm thuần khiết là một hàm mà kết quả của nó chỉ phụ thuộc vào các tham số đầu vào và hoạt động của nó không tạo ra hiệu ứng phụ (side effect) nào, nghĩa là, không tạo ra tác động bên ngoài nào ngoài giá trị trả về.

Vẻ đẹp của một hàm thuần khiết là ở sự đơn giản trong kiến trúc. Bởi vì một hàm thuần khiết bị giảm chỉ còn các đối số và giá trị trả về (nghĩa là API của nó), nó có thể được coi là một ngõ cụt phức tạp: Tương tác duy nhất của nó với hệ thống bên ngoài mà nó hoạt động là thông qua API được xác định.

Điều này trái ngược với OOP nơi các phương thức đối tượng được thiết kế để tương tác với trạng thái của đối tượng (các thành viên đối tượng) và ngược lại với mã kiểu thủ tục nơi trạng thái bên ngoài thường được thao tác từ bên trong hàm.

Tuy nhiên, trong thực tế, các hàm thường kết thúc bằng việc tương tác với ngữ cảnh rộng hơn, được chứng minh bằng useEffect hook của React.

Tính bất biến (Immutability)

Một nguyên lý khác của triết lý lập trình hàm là không sửa đổi dữ liệu bên ngoài hàm. Trong thực tế, điều này có nghĩa là tránh sửa đổi các đối số đầu vào cho một hàm. Thay vào đó, giá trị trả về của hàm phải phản ánh công việc đã thực hiện. Đây là một cách để tránh tác dụng phụ. Nó làm cho nó dễ dàng hơn để suy luận về các tác động của hàm khi nó hoạt động trong hệ thống lớn hơn.

Hàm hạng nhất (First class functions)

Ngoài lý tưởng về hàm thuần khiết, trong thực tế lập trình chàm xoay quanh các hàm hạng nhất. Một hàm hạng nhất là một hàm được coi như một “vật tự thân”, có khả năng đứng một mình và được xử lý độc lập. Lập trình hàm tìm cách tận dụng sự hỗ trợ của ngôn ngữ trong việc sử dụng các hàm làm biến, đối số và giá trị trả về để tạo mã thanh lịch.

Bởi vì các hàm hạng nhất rất linh hoạt và hữu ích, ngay cả các ngôn ngữ OOP mạnh mẽ như Java và C # cũng đã chuyển sang kết hợp hỗ trợ hàm hạng nhất. Đó là động lực đằng sau sự hỗ trợ của Java 8 cho các biểu thức Lambda.

Một cách khác để mô tả các hàm lớp đầu tiên là các hàm dưới dạng dữ liệu. Có nghĩa là, một hàm lớp đầu tiên có thể được gán cho một biến giống như bất kỳ dữ liệu nào khác. Khi bạn viết let myFunc = function () {}, bạn đang sử dụng một hàm làm dữ liệu.

Hàm bậc cao (Higher-order functions)

Một hàm chấp nhận một hàm làm đối số hoặc trả về một hàm, được gọi là một hàm bậc cao hơn – một hàm hoạt động dựa trên một hàm.

Cả JavaScipt và Java đều đã bổ sung thêm cú pháp hàm cải tiến trong những năm gần đây. Java đã thêm toán tử mũi tên và toán tử dấu hai chấm (arrow operator & double colon operator). JavaScript đã thêm toán tử mũi tên. Các toán tử này được thiết kế để giúp việc xác định và sử dụng các hàm dễ dàng hơn, đặc biệt là các hàm nội tuyến (inline) dưới dạng các hàm ẩn danh (anonymous functions). Một hàm ẩn danh là một hàm được định nghĩa và sử dụng mà không được cung cấp một biến tham chiếu.

Ví dụ về lập trình hàm: Collection

Có lẽ ví dụ nổi bật nhất về nơi mà lập trình hàm tỏa sáng là xử lý các tập hợp. Điều này là do có thể áp dụng các khối chức năng trên các items trong collection (bộ sưu tập) là điều hoàn toàn phù hợp với ý tưởng hàm thuần túy.

Hãy xem xét Listing 1, sử dụng hàm map () của JavaScript để viết hoa các chữ cái trong một mảng.

Listing 1. Dùng hàm map() và hàm ẩn danh trong JavaScript

let letters = ["a", "b", "c"];
console.info( letters.map((x) => x.toUpperCase()) ); // outputs ["A", "B", "C"]

Vẻ đẹp của cú pháp này là code được tập trung chặt chẽ. Không yêu cầu thao tác vòng lặp và mảng. Quá trình suy nghĩ về những gì đang được thực hiện được thể hiện rõ ràng bằng mã này. Điều tương tự cũng đạt được với toán tử mũi tên của Java như trong Listing 2.

Listing 2. sử dụng map() và hàm ẩn danh trong Java

import java.util.*; 
import java.util.stream.Collectors;
import static java.util.stream.Collectors.toList;
//...
List lower = Arrays.asList("a","b","c");
System.out.println(lower.stream().map(s -> s.toUpperCase()).collect(toList())); // outputs ["A", "B", "C"]

Listing 2 sử dụng thư viện luồng (stream library) của Java 8 để thực hiện cùng một tác vụ viết hoa danh sách các chữ cái. Lưu ý rằng cú pháp của toán tử mũi tên cốt lõi hầu như giống với JavaScript và chúng làm điều tương tự, tức là tạo một hàm chấp nhận các đối số, thực hiện logic và trả về một giá trị. (Điều quan trọng cần lưu ý là nếu thân hàm được xác định thiếu dấu ngoặc nhọn xung quanh nó, thì giá trị trả về sẽ tự động được cung cấp.)

Tiếp tục với Java, hãy xem xét toán tử dấu hai chấm trong Listing 3. Toán tử này cho phép bạn tham chiếu một phương thức trên một lớp: trong trường hợp này là phương thức toUpperCase trên lớp String. Listing 3 làm điều tương tự như Listing 2. Các cú pháp khác nhau có ích cho các tình huống khác nhau.

Listing 3. Toán tử dấu hai chấm trong Java (Double Colon Operator)

// ...
List upper = lower.stream().map(String::toUpperCase).collect(toList());

Trong cả ba ví dụ trên, bạn có thể thấy rằng các hàm bậc cao đang hoạt động. Hàm map () trong cả hai ngôn ngữ đều chấp nhận một hàm làm đối số.

Nói một cách khác, bạn có thể xem việc chuyển các hàm vào các hàm khác (trong Array API hoặc cách khác) dưới dạng các giao diện chức năng. Các hàm của trình cung cấp (sử dụng các hàm tham số) là các trình cắm thêm vào logic tổng quát.

Điều này trông giống như một mô hình chiến lược trong OOP (và thực sự, trong Java, một giao diện với một phương thức duy nhất được tạo ra bên dưới) nhưng sự nhỏ gọn của một hàm tạo nên một giao thức thành phần rất chặt chẽ.

Như một ví dụ khác, hãy xem xét Listing 4, định nghĩa một trình xử lý tuyến trong khung Express cho Node.js.

Listing 4. Route handler trong Express

var express = require('express');
var app = express();
app.get('/', function (req, res) {
  res.send('One Love!');
});

Listing 4 là một ví dụ điển hình về lập trình hàm trong đó nó cho phép định nghĩa rõ ràng về chính xác những gì cần thiết để ánh xạ một tuyến đường và xử lý các yêu cầu và phản hồi – mặc dù có thể lập luận rằng thao tác đối tượng phản hồi trong thân hàm là một ảnh hưởng phụ

Các hàm Curry (Curried functions)

Bây giờ hãy xem xét khái niệm lập trình hàm của các hàm trả về các hàm. Điều này ít gặp hơn so với các hàm dưới dạng đối số. Liệt kê 5 có một ví dụ từ một mẫu React phổ biến, trong đó cú pháp mũi tên béo (fat-arrow) được xâu chuỗi.

Listing 5. Hàm curry trong React

handleChange = field => e => {
e.preventDefault();
// Handle event
}

Mục đích của phần trên là tạo một trình xử lý sự kiện sẽ chấp nhận trường được đề cập và sau đó là sự kiện. Điều này rất hữu ích vì bạn có thể áp dụng cùng một handleChange cho nhiều trường. Nói tóm lại, cùng một trình xử lý có thể sử dụng được trên nhiều trường.

Listing 5 là một ví dụ về hàm curried. “Chức năng nấu cà ri” là một cái tên hơi khó chịu. Nó tôn vinh một người, điều này thật tốt, nhưng nó không mô tả khái niệm, điều này gây nhầm lẫn. Trong mọi trường hợp, ý tưởng là khi bạn có các hàm trả về các hàm, bạn có thể xâu chuỗi các lệnh gọi chúng lại với nhau, theo cách linh hoạt hơn so với việc tạo một hàm đơn lẻ với nhiều đối số.

Khi gọi các loại hàm này, bạn sẽ gặp phải cú pháp “dấu ngoặc đơn được xâu chuỗi” đặc biệt: handleChange (trường) (sự kiện).

Những lợi ích lớn hơn của lập trình hàm

Các ví dụ trước cung cấp sự hiểu biết thực tế về lập trình hàm trong bối cảnh tập trung, nhưng lập trình hàm nhằm mang lại lợi ích lớn hơn cho việc lập trình nói chung. Nói cách khác, lập trình hàm nhằm mục đích tạo ra các hệ thống quy mô lớn, linh hoạt hơn, sạch hơn.

Thật khó để cung cấp ví dụ về điều này, nhưng một ví dụ trong thế giới thực là động thái của React nhằm thúc đẩy các functional components. Đội ngũ React đã lưu ý rằng functional style of component mang lại lợi ích kết hợp khi kiến trúc giao diện phát triển lớn hơn.

Một hệ thống khác sử dụng nhiều lập trình hàm là ReactiveX. Các hệ thống quy mô lớn được xây dựng dựa trên loại luồng sự kiện mà ReactiveX sử dụng có thể được hưởng lợi từ tương tác thành phần phần mềm được tách rời. Angular hoàn toàn sử dụng ReactiveX (RxJS) trên diện rộng như một sự thừa nhận về sức mạnh này.

Phạm vi biến và ngữ cảnh

Cuối cùng, một vấn đề rất quan trọng cần chú ý khi thực hiện lập trình hàm, đó là phạm vi biến (variable scope) và ngữ cảnh (context).

Trong JavaScript, context (ngữ cảnh) có nghĩa cụ thể là những gì từ khóa this giải quyết. Trong trường hợp của toán tử mũi tên JavaScript, this đề cập đến ngữ cảnh bao quanh. Một hàm được xác định với cú pháp truyền thống sẽ nhận được ngữ cảnh riêng của nó. Trình xử lý sự kiện trên các đối tượng DOM có thể tận dụng thực tế này để đảm bảo rằng từ khóa this đề cập đến phần tử đang được xử lý.

Scope (phạm vi) đề cập đến variable horizon, tức là những biến nào có thể nhìn thấy được. Trong trường hợp của tất cả các hàm JavaScript (cả hàm béo và hàm truyền thống) và trong trường hợp các hàm ẩn danh do mũi tên xác định của Java, phạm vi là phạm vi của nội dung hàm bao quanh. Đây là lý do tại sao các hàm như vậy được gọi là closures. Thuật ngữ này có nghĩa là hàm được bao bọc trong phạm vi chứa của nó.

Điều quan trọng cần nhớ là: Các hàm ẩn danh như vậy có toàn quyền truy cập vào các biến trong phạm vi. Hàm bên trong có thể hoạt động dựa trên các biến của hàm bên ngoài. Đây có thể được coi là một tác dụng phụ của hàm không thuần túy.

Theo: https://www.infoworld.com/article/3613715/what-is-functional-programming-a-practical-guide.html

Theo: https://kipalog.com/posts/Functional-Programming—Phan-1—Con-duong-sang

Đánh giá bài viết

Average rating 0 / 5. Vote count: 0

No votes so far! Be the first to rate this post.

Comments are closed.