6.1 – Closures


Khi một hàm được viết trong một hàm khác, nó có toàn quyền truy cập vào các biến cục bộ từ hàm bao quanh; tính năng này được gọi là phạm vi từ vựng. Mặc dù điều đó nghe có vẻ hiển nhiên nhưng thực tế không phải vậy. Phạm vi từ vựng, cộng với các hàm hạng nhất, là một khái niệm mạnh mẽ trong một ngôn ngữ lập trình, nhưng rất ít ngôn ngữ hỗ trợ khái niệm đó.

Hãy để chúng tôi bắt đầu với một ví dụ đơn giản. Giả sử bạn có một danh sách tên học sinh và một bảng liên kết tên với điểm; bạn muốn sắp xếp danh sách tên, theo lớp của chúng (lớp cao hơn trước). Bạn có thể thực hiện tác vụ này như sau:

names = {"Peter", "Paul", "Mary"}
    grades = {Mary = 10, Paul = 7, Peter = 8}
    table.sort(names, function (n1, n2)
      return grades[n1] > grades[n2]    -- compare the grades
    end)

Bây giờ, giả sử bạn muốn tạo một hàm để thực hiện tác vụ này:

 function sortbygrade (names, grades)
      table.sort(names, function (n1, n2)
        return grades[n1] > grades[n2]    -- compare the grades
      end)
    end

Điểm thú vị trong ví dụ này là hàm ẩn danh được cung cấp để sắp xếp truy cập vào các cấp tham số, là địa phương cho phép sắp xếp hàm bao quanh. Bên trong hàm ẩn danh này, điểm không phải là biến toàn cục cũng không phải là biến cục bộ. Chúng tôi gọi nó là một biến cục bộ bên ngoài, hoặc một giá trị tăng. (Thuật ngữ “upvalue” hơi gây hiểu nhầm vì điểm là một biến chứ không phải một giá trị. Tuy nhiên, thuật ngữ này có nguồn gốc lịch sử từ Lua và nó ngắn hơn “biến cục bộ bên ngoài”.)

Tại sao điều đó lại thú vị như vậy? Bởi vì các hàm là các giá trị hạng nhất. Hãy xem xét đoạn mã sau:

 function newCounter ()
      local i = 0
      return function ()   -- anonymous function
               i = i + 1
               return i
             end
    end
    
    c1 = newCounter()
    print(c1())  --> 1
    print(c1())  --> 2

Bây giờ, hàm ẩn danh sử dụng giá trị tăng, i, để giữ bộ đếm của nó. Tuy nhiên, vào thời điểm chúng tôi gọi hàm ẩn danh, tôi đã ở ngoài phạm vi, vì hàm tạo ra biến đó (newCounter) đã trả về. Tuy nhiên, Lua xử lý tình huống đó một cách chính xác, sử dụng khái niệm đóng cửa. Nói một cách đơn giản, bao đóng là một hàm cộng với tất cả những gì nó cần để truy cập chính xác các giá trị tăng của nó. Nếu chúng ta gọi lại newCounter, nó sẽ tạo một biến cục bộ mới i, vì vậy chúng ta sẽ nhận được một bao đóng mới, tác động lên biến mới đó:

    c2 = newCounter()
    print(c2())  --> 1
    print(c1())  --> 3
    print(c2())  --> 2

Vì vậy, c1 và c2 là các đóng khác nhau trên cùng một hàm và mỗi hàm hoạt động dựa trên một khởi tạo độc lập của biến cục bộ i. Về mặt kỹ thuật, một giá trị trong Lua là bao hàm, không phải hàm. Bản thân hàm chỉ là một nguyên mẫu cho các bao đóng. Tuy nhiên, chúng tôi sẽ tiếp tục sử dụng thuật ngữ “chức năng” để chỉ việc đóng bất cứ khi nào không có khả năng xảy ra nhầm lẫn.

Closures cung cấp một công cụ có giá trị trong nhiều bối cảnh. Như chúng ta đã thấy, chúng hữu ích như là đối số cho các hàm bậc cao hơn như sắp xếp. Đóng cửa cũng có giá trị cho các chức năng xây dựng các chức năng khác, chẳng hạn như ví dụ newCounter của chúng tôi; cơ chế này cho phép các chương trình Lua kết hợp các kỹ thuật lập trình ưa thích từ thế giới chức năng. Closures cũng hữu ích cho các chức năng gọi lại. Ví dụ điển hình ở đây xảy ra khi bạn tạo các nút trong bộ công cụ GUI điển hình. Mỗi nút có chức năng gọi lại sẽ được gọi khi người dùng nhấn nút; bạn muốn các nút khác nhau thực hiện những việc hơi khác khi được nhấn. Ví dụ, một máy tính kỹ thuật số cần mười nút tương tự, một nút cho mỗi chữ số. Bạn có thể tạo mỗi người trong số họ với một chức năng như chức năng tiếp theo:

function digitButton (digit)
      return Button{ label = digit,
                     action = function ()
                                add_to_display(digit)
                              end
                   }
    end

Trong ví dụ này, chúng tôi giả định rằng Nút là một chức năng của bộ công cụ tạo các nút mới; label là nhãn nút; và action là chức năng gọi lại sẽ được gọi khi nhấn nút. (Nó thực sự là một bao đóng, bởi vì nó truy cập vào chữ số giá trị tăng.) Hàm gọi lại có thể được gọi trong một thời gian dài sau khi digitButton thực hiện nhiệm vụ của nó và sau khi chữ số biến cục bộ ra khỏi phạm vi, nhưng nó vẫn có thể truy cập vào biến đó.

Đóng cửa cũng có giá trị trong một bối cảnh hoàn toàn khác. Bởi vì các hàm được lưu trữ trong các biến thông thường, chúng ta có thể dễ dàng xác định lại các hàm trong Lua, thậm chí là các hàm được xác định trước. Cơ sở này là một trong những lý do khiến Lua rất linh hoạt. Tuy nhiên, thông thường, khi bạn xác định lại một chức năng, bạn cần chức năng ban đầu trong việc triển khai mới. Ví dụ: giả sử bạn muốn xác định lại hàm sin để hoạt động theo độ thay vì radian. Hàm mới này phải chuyển đổi đối số của nó, và sau đó gọi hàm sin ban đầu để thực hiện công việc thực sự. Mã của bạn có thể trông giống như

oldSin = math.sin
    math.sin = function (x)
      return oldSin(x*math.pi/180)
    end

Một cách rõ ràng hơn để làm điều đó như sau:

    do
      local oldSin = math.sin
      local k = math.pi/180
      math.sin = function (x)
        return oldSin(x*k)
      end
    end

Bây giờ, chúng tôi giữ phiên bản cũ trong một biến riêng; cách duy nhất để truy cập nó là thông qua phiên bản mới.

Bạn có thể sử dụng tính năng tương tự này để tạo môi trường an toàn, còn được gọi là hộp cát. Môi trường an toàn là điều cần thiết khi chạy mã không đáng tin cậy, chẳng hạn như mã được máy chủ nhận qua Internet. Ví dụ: để hạn chế các tệp mà một chương trình có thể truy cập, chúng ta có thể xác định lại hàm mở (từ thư viện io) bằng cách sử dụng các bao đóng:

     do
      local oldOpen = io.open
      io.open = function (filename, mode)
        if access_OK(filename, mode) then
          return oldOpen(filename, mode)
        else
          return nil, "access denied"
        end
      end
    end

Điều làm cho ví dụ này hay là, sau lần định nghĩa lại đó, không có cách nào để chương trình gọi là mở không hạn chế, ngoại trừ thông qua phiên bản mới, bị hạn chế. Nó giữ phiên bản không an toàn dưới dạng một biến riêng trong trạng thái đóng, không thể truy cập từ bên ngoài. Với tiện ích này, bạn có thể xây dựng các hộp cát Lua trong chính Lua, với lợi ích thông thường: tính linh hoạt. Thay vì một giải pháp phù hợp với tất cả, Lua cung cấp cho bạn một cơ chế meta để bạn có thể điều chỉnh môi trường của mình cho các nhu cầu bảo mật cụ thể của bạn.

Leave a comment