Trả hàm về từ phạm vi lồng nhau

Một phần của tài liệu bài giảng môn nguyên lý các ngôn ngữ lập trình C4 (Trang 35 - 40)

Một vấn đề liên quan nhưng phức tạp hơn là đôi khi được gọi là upward funnarg problem, mặc dù có thể gọi chính xác hơn là vấn đề kết quả - hàm phía trên, vì nó xảy ra khi trả về giá trị hàm từ phạm vi lồng nhau, nói chung như giá trị trả về của hàm số. Ví dụ đơn giản của hàm mà trả về hàm là mã ML sau cho việc hợp hàm:

Cho hai đối số hàm số f và g, phép hợp hàm trả về hàm hợp của f và g. Thân của phép hợp là mã yêu cầu tham số hàm x và sau đó tính toán g(f(x)). Mã này có ích chỉ khi nó gắn kết với cơ chế nào đó để tìm giá trị của f và g. Do đó, bao đóng được sử dụng để thể hiện phép hợp hàm (f,g). Con trỏ mã của bao đóng này trỏ tới mã dịch “để nhận tham số x của hàm và sau đó tính toán g(f(x))” và con trỏ bản ghi kích hoạt của bao đóng này trỏ đến bản ghi kích hoạt của phép hợp lời gọi (f,g) vì vậy bản ghi kích hoạt này cho mã để tìm các hàm thực tế f và g cần thiết để tính hợp hàm của chúng.

Chúng ta sẽ sử dụng ví dụ hàm gây ngạc nhiên một chút để thấy các bao đóng giải quyết vấn đề tìm kiếm biến như thế nào khi các hàm được trả về từ phạm vi lồng nhau. Ví dụ sau có thể cho cảm giác về sự tương tự giữa bao đóng và đối tượng.

Ví dụ 7.9

Trong mã ví dụ này, counter là hàm mà có giá trị nguyên được lưu trữ. Khi được gọi, counter tăng giá trị bên trong của nó và trả về gía trị mới. Giá trị mới này trở thành giá trị riêng được lưu trữ cho lời gọi tiếp theo. Hàm ML make_counter sau, nhận đối số nguyên và trả về counter khởi tạo giá trị nguyên này:

Hàm make_counter cấp bộ nhớ cho biến cục bộ count, khởi tạo bằng giá trị của tham số nguyên init. Hàm make_counter sau đó trả về một hàm, mà khi được gọi, sẽ tăng thêm giá trị của count bằng tham số inc, và sau đó trả về giá trị mới của count.

Sau đây là cũng là chương trình đó được viết trên cú pháp tựa C cho những ai thích dùng nó hơn:

Nếu ta lần theo việc cấp bộ nhớ gắn kết với quá trinh dịch và thực thi, ta có thể thấy là nguyên lý ngăn xếp không còn đúng nữa. Cụ thể, cần thiết lưu bản ghi kích hoạt mà có thể được lấy ra từ ngăn xếp, nếu ta tuân theo nguyên lý chuẩn cấp sau-giải phóng trước.

Hình 4-10 chỉ ra rằng, các bản ghi được cấp và giải phóng khi thực thi mã từ ví dụ 4- 9. Sau đây là dãy các bước để tạo ra các bản ghi chỉ ra trên Hình 4-10.

1. Khai báo của make_counter. Một bản ghi kích hoạt cho khối bao gồm việc khai báo hàm make_counter được cấp trên ngăn xếp thời gian chạy. Tên hàm dùng cho counter đươc viết ngắn ở đây là make_c để tên trùng với trên hình vẽ. Giá trị của make_c là bao đóng. Con trỏ bản ghi kích hoạt của bao đóng này trỏ này trỏ đến bản ghi kích hoạt của make_counter. Con trỏ mã của bao đóng trỏ đến trỏ đến mã của make_counter được tạo trong thời gian dịch.

2. Khai báo của c: Bản ghi kích hoạt của khối gồm khai báo hàm c được cấp trên ngăn xếp thời gian chạy. Con trỏ truy cập của bản ghi kích hoạt này trỏ bản ghi kích hoạt dầu tiên , như khai báo của make_counter là khối trước đó. Giá trị của c là hàm biểu diễn bởi bao đóng. Tuy nhiên, biểu thức định nghĩa hàm c không phải là khai báo hàm. Đây là biểu thức mà gửi mà lời gọi yêu cầu đến hàm make_counter. Do đó, sau khi bản ghi kích hoạt này được thiết lập, chương trình sẽ thực hiện và cần phải gọi đến make_counter.

3. Lời gọi đến make_counter: Một bản ghi kích hoạt được cấp để thực thi hàm make_counter. Bản ghi kích hoạt này chứa không gian cho tham số mà sẽ là kết quả trả về của lời gọi. Con trỏ mã của bao đóng cho counter sẽ trỏ đến mã mà được sinh ra và lưu trong thời gian dịch. Con trỏ bản ghi

kích hoạt trỏ đến bản ghi kích hoạt thứ ba, vì biến cục bộ của hàm thường trú ở đây. Chương trình vẫn cần bản ghi kích hoạt ba sau lời gọi đến make_counter được trả về, vì counter của hàm trả về từ lời gọi đến make_counter tham chiếu đến các biến init và count mà được lưu trong (hoặc đạt được thông qua) bản ghi kích hoạt cho make_counter. Nếu bản ghi kích hoạt ba (sử dụng để gọi đến make_counter được lấy ra từ ngăn xếp), hàm counter sẽ làm việc không đúng đắn.

4. Lời gọi đầu tới c(2): Khi biểu thức đầu c(2) được tính toán, bản ghi kích hoạt được tạo ra cho lời gọi hàm. Bản ghi kích hoạt này có con trỏ truy cập được thiết lập từ bao đóng cho c. Vì bao đóng trỏ đến bản ghi kích hoạt ba, nên con trỏ truy cập cho bản ghi kích hoạt 4 cũng vậy. Điều này quan trọng vì mã cho counter tham chiếu đến biến count và count được cấp theo con trỏ trong bản ghi kích hoạt ba.

Hình 4.10: Các bản ghi kích hoạt cho bao đóng hàm trả về từ hàm.

Ở đây có hai điểm chính cần nhớ trong ví dụ này:

• Bao đóng được dùng để thiết lập con trỏ truy cập khi hàm trả về từ phạm vi

lồng nhau được gọi

• Khi hàm được trả về từ phạm vi lồng nhau, các bản ghi kích hoạt không cần

tuân theo nguyên lý ngăn xếp. Bản ghi kích hoạt được cấp với lời gọi hàm không thể được giải phóng khi hàm trả về, nếu giá trị trả về của hàm là một hàm khác mà có thể đòi hỏi bản ghi kích hoạt này.

Giải pháp cho vấn đề quản trị bộ nhớ.

Bạn có thể nhận thấy rằng, sau khi mã trong Ví dụ 4.9 được thực hiện, theo các bước mới được mô tả, chúng ta còn một số bản ghi kích hoạt còn lại trên ngăn xếp mà có thể được sử dụng trong phần còn lại của chương trình. Cụ thể nếu hàm c được gọi nữa trong một biểu thức khác, thì chúng ta cần các bản ghi kích hoạt này mà duy trì phạm vi tĩnh của nó. Tuy nhiên, nếu hàm c không được sử dụng nữa, thì chúng ta không cần các bản ghi kích hoạt này. Ta có thể hỏi, chương trình dịch hoặc hệ thống thời gian chạy xác định như thế nào về việc khi nào một bản ghi kích hoạt có thể được giải phóng.

Có một số phương pháp giải quyết vấn đề này. Tuy nhiên, việc bàn luận đầy đủ là phức tạp và không cần thiết để hiểu các cân nhắc thiết kế ngôn ngữ cơ bản mà là đối tượng của chương này. Một giải pháp mà là tương đối trực tiếp và không phải là không hiệu quả là đơn giản sử dụng thuật toán thu gom rác để tìm các bản ghi mà không cần nữa. Trong cách tiếp cận này, thu gom rác sẽ lần theo các con trỏ đến các bản ghi kích hoạt và thu gom các bản ghi không đạt được khi mà không có các con trỏ bao đóng nào trỏ đến chúng.

2.5. Tóm tắt chương

Một khối là một vùng văn bản chương trình, được xác định bằng các dấu bắt đầu và kết thúc, mà có thể chứa khai báo cục bộ trong vùng này. Các khối có thể xuất hiện bên trong khối khác hoặc như thân của một hàm hay một thủ tục. Các ngôn ngữ cấu trúc khối được cài đặt bởi các bản ghi kích hoạt mà chứa không gian cho các biến cục bộ hoặc thông tin liên quan đến khối khác. Vì chỉ có một cách để cho một khối chồng chéo lên khối khác là một khối chứa trọn vẹn khối kia. Các bản ghi kích hoạt nói chung được quản lý trên ngăn xếp thời gian chạy với chính sách bản ghi kích hoạt mới được cấp sẽ được giải phóng trước tiên (last allocated – first deallocated).

Các tham số được truyền cho hàm và thủ tục được lưu trong các bản ghi kích hoạt, cũng như các biến cục bộ. Bản ghi kích hoạt có thể chứa giá trị tham số thực tế (truyền theo giá trị) hoặc địa chỉ (truyền theo tham chiếu). Lời gọi đuôi có thể có thể được tối ưu để tránh trả về các thủ tục gọi. Trong trường hợp hàm đệ qui đuôi mà không truyền đối số hàm, điều đó có nghĩa là cùng một bản ghi kích hoạt có thể được dùng cho mọi lời gọi đệ qui, loại bỏ việc cần thiết phải cấp, khởi tạo, và sau đó giải phóng bản ghi kích hoạt cho mỗi lời gọi. Kết quả là hàm đệ qui đuôi có thể được thực hiện cũng hiệu quả như vòng lặp.

Truy cập đúng đến các biến tổng thể phạm vi tĩnh bao gồm ba khái niệm cài đặt sau:

• Các bản ghi kích hoạt của các hàm và các thủ tục chứa các liên kết truy cập

(phạm vi tĩnh) mà trỏ đến các bản ghi kích hoạt liên kết với khối bao trung gian.

• Các hàm được truyền như các tham số hoặc trả về như kết quả cần được biểu diễn dạng bao đóng (closures) gồm mã hàm cùng với con trỏ đến môi trường từ vựng đúng.

• Khi hàm trả về hàm mà dựa trên các biến được khai báo trong phạm vi lồng

nhau, phạm vi tĩnh đòi hòi suy diễn từ nguyên lý ngăn xếp: Bản ghi kích hoạt có thể cần được duy trì cho đến khi giá trị hàm (bao đóng) không cần sử dụng nữa trong chương trình.

Mỗi khái niệm đặt được bàn trong chương này, có thể được gắn chặt với một tính chất của ngôn ngữ cụ thể, được tổng kết trong bảng sau:

Các bài tập

trên việc bản ghi kích hoạt cho khối mà ở đó x được khai báo được đẩy vào trên, ngăn xếp.

Một phần của tài liệu bài giảng môn nguyên lý các ngôn ngữ lập trình C4 (Trang 35 - 40)