Bao đóng (lập trình máy tính)

Trong khoa học máy tính, bao đóng (closure) là một hàm hay một tham chiếu tới một hàm cùng với môi trường tham chiếu - một bảng chứa tham chiếu đến mỗi biến không phải cục bộ (hay còn gọi là biến tự do). Closure còn được gọi với tên là lexical closure (bao đóng hay function closure (bao đóng hàm). Bao đóng khác với một con trỏ hàm thuần túy ở chỗ nó cho phép một hàm có thể truy cập các biến không phải cục bộ ngay cả khi hàm này được gọi ngoài phạm vi của nó.

Ví dụ
Đoạn mã Python dưới đây định nghĩa một hàm counter với một biến cục bộ x và một hàm lồng (nested function) increment

def counter():
x = 0
def increment(y):
nonlocal x
x += y
print(x)
return increment

Bao đóng được trả lại bởi counter có thể được gán cho một biến:

counter1_increment = counter()
counter2_increment = counter()

Khi chúng ta gọi hàm increment thông qua bao đóng sẽ nhận được kết quả như sau:

counter1_increment(1) in ra kết quả 1
counter1_increment(7) in ra kết quả 8
counter2_increment(1) in ra kết quả 1
counter1_increment(1) in ra kết quả 9

Sự khác biệt về ngữ cảnh
Phạm vi tác dụng
Các ngôn ngữ lập trình khác nhau không phải lúc nào cũng thống nhất về lexical environment(tạm dịch: phạm vi tác dụng) và định nghĩa về bao đóng trong các ngôn ngữ vì thế cũng có thể khác nhau. Định nghĩa đơn giản nhất của lexical environment chỉ ra một tập các binding của các biến trong một phạm vi nào đó. Đó cũng chính là điều bao đóng muốn lưu giữ (capture) lại.
Ý nghĩa binding (tạm dịch: gắn kết) trong các ngôn ngữ cũng khác nhau.
Trong các ngôn ngữ mệnh lệnh (imperative languages) các biến được gắn kết đến các vùng bộ nhớ lưu trữ giá trị của các biến đó. Việc lưu giữ lại các biến kiểu này được gọi là lưu giữ bằng tham chiếu (capturing by reference). Đoạn mã sau miêu tả một ví dụ trong ECMAScript

// ECMAScript
var f, g;
function foo() {
var x = 0;
f = function() { return ++x; };
g = function() { return --x; };
x = 1;
alert('Bên trong foo, gọi hàm f(): ' + f()); // hiện ra giá trị "2"
}
foo();
alert('Gọi hàm g(): ' + g()); // hiện ra giá trị "1"
alert('Gọi hàm f(): ' + f()); // hiện ra giá trị "2"

Trong ví dụ trên hàm foo và các bao đóng được tham chiếu bởi các biến f và biến g đều sử dụng chung một vùng bộ nhớ khi truy cập biến x. Chính vì vậy giá trị của x đều thay đổi sau mỗi lần gọi hàm foo hay gọi các hàm qua các bao đóng f và biến g

Trong khi đó rất nhiều các ngôn ngữ lập trình khác, ví dụ như ngôn ngữ ML, việc gắn kết biến lại đi liền trực tiếp với giá trị của biến. Kiểu lưu giữ này được gọi là lưu giữ bằng giá trị (capturing by value)

Một số ngôn ngữ cho phép chúng ta lựa chọn kiểu lưu trữ theo ý muốn. Ví dụ trong ngôn ngữ C++11 hay PHP, chúng ta sử dụng từ khóa & để lưu trữ biến theo tham chiếu. Khi không dùng từ khóa đó, các biến sẽ được lưu trữ theo giá trị.

Thoát khỏi phạm vi tác dụng
Các ngôn ngữ lập trình khác nhau cũng có sự khác nhau trong việc thoát khỏi phạm vi tác dụng xác định bởi các lệnh của ngôn ngữ đó, ví dụ: return, break hay continue.
Ví dụ sau chỉ ra sự khác biệt trong hai ngôn ngữ Smalltalk và ECMAScript:

"Smalltalk"
foo
| xs |
xs:= (1 2 3 4).
xs do: [:x | ^x].
^0
bar
Transcript show: (self foo printString) "in ra kết quả 1"

// ECMAScript
function foo() {
var xs = [1, 2, 3, 4];
xs.forEach(function (x) { return x; });
return 0;
}
alert(foo()); // in ra kết quả 0

Các đoạn mã trên sẽ cho ra các kết quả khác nhau vì toán tử ^ của Smalltalk thực thi khác với toán tử return trong EMCAScript. Toán tử return trong EMCAScript sẽ thoát ra khỏi bao đóng phía trong và bắt đầu một vòng lặp mới của vòng lặp forEach trong khi toán tử ^ thực thi trên biến x (lệnh ^x sẽ kết thúc vòng lặp do và trả lại chương trình từ hàm foo.

Tham khảo