You are on page 1of 30

abstract machine không chạy cụ thể trên máy nào, nhưng có thể thực

thi trên nó bằng cách diễn giải.


Mọi chuyên ngành khoa học đều mô tả một hiện tượng nhất định, chỉ
tập trung vào một số khía cạnh, những khía cạnh được cho là phù
hợp nhất với mục tiêu đã thống nhất.
tính biểu cảm phụ thuộc một cách thiết yếu vào các cơ chế trừu
tượng mà các ngôn ngữ cung cấp
Các cơ chế này là công cụ chính có sẵn cho người thiết kế và người
lập trình để mô tả một cách chính xác, nhưng cũng đơn giản và gợi
mở, mức độ phức tạp của các vấn đề cần giải quyết
Nói chung, trong một ngôn ngữ lập trình, hai lớp cơ chế trừu tượng
được phân biệt. Cái cung cấp sự trừu tượng hóa điều khiển và cái
cung cấp sự trừu tượng hóa dữ liệu.
Cái trước cung cấp cho người lập trình khả năng ẩn dữ liệu thủ tục;
-> chương này tập trung cái này.
thứ hai cho phép định nghĩa và sử dụng các kiểu dữ liệu phức tạp mà
không cần đề cập đến cách các kiểu đó sẽ được thực hiện.
7.1 Subprograms

Việc phân tách một vấn đề thành các bài toán con cho phép quản lý
độ phức tạp tốt hơn. Một vấn đề hạn chế hơn sẽ dễ giải quyết hơn.
Giải pháp cho vấn đề toàn cầu có được nhờ thành phần thích hợp của
các giải pháp cho các vấn đề con này.
Một hàm là một đoạn mã được xác định bằng tên, được cung cấp một
môi trường cục bộ của riêng nó và có thể trao đổi thông tin với phần
còn lại của mã bằng cách sử dụng các tham số.
Trong hình 7.1, năm dòng đầu tiên tạo thành định nghĩa của hàm có
tên foo, có môi trường cục bộ được tạo thành từ ba tên n, a và tmp.
2 Dòng đầu tiên là tiêu đề, trong khi các dòng còn lại tạo thành phần
nội dung của hàm . Hai dòng cuối của Hình 7.1 là cách sử dụng (hoặc
cách gọi) foo.

Một hàm trao đổi thông tin với phần còn lại của chương trình bằng ba
cơ chế chính:
tham số,
giá trị trả về,
môi trường non_local
Parameters:
formal param
actual param.
Các tham số chính thức luôn là những tên, đối với môi trường, hoạt
động như các khai báo cục bộ cho chính hàm.
Tên foo là một phần của môi trường non local của hàm
Đôi khi cũng có khả năng khai báo các hàm với một số lượng tham số
thay đổi.
ví dụ ??
Return value
ví dụ, cấu trúc trả về cũng có tác dụng chấm dứt việc thực thi hàm
hiện tại
Trong một số ngôn ngữ, tên “function” được dành riêng cho các
chương trình con trả về một giá trị, trong khi những chương trình con
tương tác với trình gọi chỉ thông qua các tham số hoặc môi trường
không cục bộ được gọi là “thủ tục”
Nonlocal environment: Đây là một cơ chế ít phức tạp hơn mà một
hàm có thể trao đổi thông tin với phần còn lại của chương trình. Nếu
phần thân của hàm sửa đổi một biến non-local, thì rõ ràng rằng việc
sửa đổi như vậy được cảm nhận trong tất cả các phần của chương
trình nơi biến này hiển thị

7.1.1 Functional Abstraction


Các khách hàng của một thành phần như vậy không quan tâm đến
cách các dịch vụ được cung cấp, chỉ quan tâm đến cách sử dụng
chúng
Khả năng liên kết một hàm với mọi thành phần cho phép tách biệt
những gì người dùng cần biết (được thể hiện trong tiêu đề của hàm:
tên, tham số, kiểu kết quả của nó, nếu có) từ những gì nó không cần
biết (nằm trong phần nội dung)
Nếu một hệ thống dựa trên sự trừu tượng hóa chức năng, ba hành
động đặc tả, thực hiện và sử dụng một chức năng có thể xảy ra độc
lập với nhau mà không cần biết về bối cảnh mà các hành động khác
được thực hiện.
ví dụ: bằng cách hạn chế các tương tác giữa hàm và lệnh gọi để
truyền tham số, bởi vì việc sử dụng môi trường non-local để trao đổi
thông tin giữa các hàm và phần còn lại của chương trình phá hủy tính
trừu tượng hóa hàm

Mặt khác, tính trừu tượng hóa hàm ngày càng được đảm bảo bởi sự
hạn chế lớn hơn của sự tương tác giữa các thành phần đối với hành vi
bên ngoài, được thể hiện bằng các tiêu đề chức năng.

7.1.2 Parameter Passing


Nguyên tắc truyền tham số.
Chế độ được cố định khi hàm được xác định và có thể khác nhau đối
với từng tham số; nó được cố định cho tất cả các lệnh gọi của hàm.
Từ quan điểm ngữ nghĩa chặt chẽ, việc phân loại kiểu giao tiếp được
cho phép bởi một tham số rất đơn giản. Từ quan điểm của chương
trình con, ba lớp tham số có thể được phân biệt:
• Input parameters.
• Output parameters.
• Input/output parameters
tham số là kiểu đầu vào nếu nó cho phép giao tiếp chỉ theo hướng từ
người gọi -> hàm.
tham số là kiểu đầu ra chỉ cho phép giao tiếp theo hướng hàm ->
người gọi.
Cuối cùng, nó là đầu vào / đầu ra khi nó cho phép giao tiếp hai chiều.
Kỹ thuật triển khai cụ thể cấu thành, chính xác, là "chế độ gọi", mà
bây giờ chúng ta sẽ phân tích, mô tả cho từng chế độ:
• Nó cho phép loại giao tiếp nào.?
• Dạng thông số thực tế nào được phép.?
• Ngữ nghĩa của chế độ.?
• việc triển khai thực thi.?
• Chi phí của nó.
Trong số các chế độ mà chúng ta sẽ thảo luận, hai chế độ đầu tiên
(theo giá trị và theo tham chiếu) là quan trọng nhất và được sử dụng
rộng rãi
giá trị
tham chiếu.
Mặc dù "gọi theo tên" hiện không được sử dụng như một cơ chế
truyền tham số, tuy nhiên, nó cho phép chúng ta trình bày một
trường hợp đơn giản về ý nghĩa của việc “truyền một môi trường”
vào một thủ tục.
môi trường -> thủ tục

Gọi theo giá trị:


Gọi theo giá trị là một chế độ tương ứng với một tham số đầu vào.
Môi trường cục bộ của thủ tục được mở rộng với sự liên kết giữa
tham số chính thức và một biến mới. Tham số thực tế có thể là một
biểu thức.
Khi được gọi, tham số thực được đánh giá và r_value của nó thu
được và được liên kết với tham số chính thức.
Khi kết thúc thủ tục, tham số chính thức bị hủy, cũng như môi trường
cục bộ của chính thủ tục
Trong quá trình thực thi phần thân, không có liên kết giữa tham số
chính thức và tham số thực tế.

Biến y không bao giờ thay đổi giá trị của nó (nó luôn giữ nguyên 1).
Trong quá trình thực thi foo, x giả định giá trị ban đầu 2 bằng tác
động của việc truyền tham số. Sau đó nó được tăng lên 3, cuối cùng
nó bị phá hủy cùng với toàn bộ bảng hoạt động của hàm foo.
Nếu chúng ta giả sử một lược đồ phân bổ dựa trên ngăn xếp, thì
tham số chính thức tương ứng với một vị trí trong bản ghi kích hoạt
của thủ tục, trong đó giá trị của tham số thực được lưu trữ trong
trình tự gọi của thủ tục.
Chúng ta hãy lưu ý rằng đây là một phương thức tốn kém khi tham số
giá trị được liên kết với một cấu trúc dữ liệu lớn. Trong trường hợp
như vậy, toàn bộ cấu trúc được sao chép sang hình thức. Mặt khác,
chi phí truy cập tham số chính thức là tối thiểu, vì nó giống như chi
phí truy cập một biến cục bộ trong phần thân.

Call by reference:
Gọi theo tham chiếu (còn được gọi theo biến) thực hiện một cơ chế
trong đó tham số có thể được sử dụng cho cả đầu vào và đầu ra.
Tham số thực tế phải là một biểu thức có l-value (nhớ lại định nghĩa
của l-value ở trang 132). Tại thời điểm gọi, l-value của tham số thực
được đánh giá và môi trường cục bộ của thủ tục được mở rộng với sự
liên kết giữa tham số chính thức và l-value của tham số thực (do đó
tạo ra alias ).
Trường hợp phổ biến nhất là trong đó tham số thực tế là một biến.
Trong trường hợp này, chính thức và thực tế là hai tên cho cùng một
biến. Khi kết thúc thủ tục, kết nối giữa tham số chính thức và l-value
của tham số thực bị hủy, cũng như môi trường cục bộ của thủ tục.
Rõ ràng rằng lệnh gọi bằng tham chiếu cho phép giao tiếp hai chiều:
mỗi sửa đổi của tham số chính thức là một sửa đổi của tham số thực.
Người đọc biết C không nên bị ngộ nhận. Trong một ngôn ngữ có con
trỏ, như chúng ta sẽ thảo luận dưới đây, thường thì việc truyền một
cấu trúc phức tạp bao gồm việc chuyển (theo giá trị) một con trỏ đến
cấu trúc dữ liệu. Trong trường hợp như vậy, nó là con trỏ được sao
chép, không phải cấu trúc dữ liệu.

Hình 7.4 cho thấy một ví dụ đơn giản về lệnh gọi bằng tham chiếu
(mà chúng tôi đã ghi chú trong mã giả với công cụ sửa đổi tham
chiếu). Trong quá trình thực hiện foo, x là tên của y. Tăng x trong
body đối với tất cả các hiệu ứng, là tăng của y. Sau cuộc gọi, giá trị
của y do đó là 1.
Có thể thấy rằng, như trong Hình 7.5, tham số thực tế không nhất
thiết phải là một biến mà có thể là một biểu thức có l-value được xác
định tại thời điểm gọi. Theo cách tương tự như trường hợp đầu tiên,
trong quá trình thực thi foo, x là tên của v [1] và số gia của x trong
phần thân, là số gia của v [1]. Do đó, sau cuộc gọi, giá trị của v [1] là
2.
Trong mô hình máy trừu tượng dựa trên ngăn xếp, mỗi biến hình
thức được liên kết với một vị trí trong bản ghi kích hoạt của thủ tục.
Trong quá trình gọi, l-value của giá trị thực được lưu trữ trong hồ sơ
kích hoạt. Truy cập vào tham số chính thức xảy ra thông qua sự
chuyển hướng sử dụng vị trí này.
Đây là một chế độ truyền tham số với chi phí rất thấp. Tại thời điểm
cuộc gọi, chỉ cần lưu trữ một địa chỉ; mọi tham chiếu đến hình thức
đều đạt được bằng một truy cập gián tiếp (được thực thi ngầm bởi
máy trừu tượng) có thể được thực hiện với chi phí rất thấp trên
nhiều kiến trúc.
Các tham số chính thức để hoán đổi (cả theo giá trị) là kiểu con trỏ
tới số nguyên (int *). Giá trị của các tham số thực là địa chỉ (nghĩa là
giá trị l) của v1 và v2 (thu được bằng cách sử dụng toán tử &). Trong
phần thân của swap, việc sử dụng toán tử * thực hiện tham chiếu các
tham số.
Ví dụ, chúng ta có thể diễn giải ý nghĩa của * a = * b là: lấy giá trị
được chứa trong vị trí có địa chỉ được chứa trong b và lưu trữ nó ở vị
trí có địa chỉ được lưu trong a. Do đó, hoán đổi của chúng tôi mô
phỏng từng cuộc gọi tham chiếu.
Call by constant
Chúng tôi đã thấy cách gọi theo giá trị tốn kém như thế nào đối với
dữ liệu lớn. Tuy nhiên, khi tham số hình thức không được sửa đổi
trong phần thân của hàm, chúng ta có thể tưởng tượng việc duy trì
ngữ nghĩa của việc truyền theo giá trị, thực hiện nó bằng cách gọi
bằng tham chiếu.
Đây là những gì cấu thành phương thức tham số chỉ đọc.
Các tham số chính thức được truyền bởi phương thức này phải tuân
theo ràng buộc tĩnh là chúng không thể được sửa đổi trong phần
thân, trực tiếp (bằng cách gán) hoặc gián tiếp (thông qua lời gọi đến
các hàm sửa đổi chúng). Từ quan điểm ngữ nghĩa, gọi theo hằng hoàn
toàn trùng khớp với gọi theo giá trị, trong khi việc lựa chọn triển khai
được để cho máy trừu tượng. Đối với dữ liệu có kích thước nhỏ, lệnh
gọi theo hằng số có thể được thực hiện dưới dạng lệnh gọi theo giá
trị.
Đối với lớn hơn cấu trúc, một triển khai sử dụng tham chiếu, không
có bản sao, sẽ được chọn.
Bằng cách đọc tiêu đề, ngay lập tức người ta có thông tin về thực tế
là tham số này chỉ là đầu vào. Hơn nữa, người ta có thể tin tưởng vào
phân tích ngữ nghĩa tĩnh để xác minh rằng chú thích này thực sự
được đảm bảo
Call by result
Gọi theo kết quả là kép chính xác của cuộc gọi theo giá trị. Đây là
một chế độ thực hiện giao tiếp chỉ đầu ra. Môi trường cục bộ của thủ
tục được mở rộng với sự liên kết giữa tham số chính thức và một
biến mới. Tham số thực tế phải là một biểu thức đánh giá giá trị l.
Khi thủ tục kết thúc (bình thường), ngay trước khi môi trường cục bộ
bị phá hủy, giá trị hiện tại của tham số chính thức được gán cho vị trí
thu được bằng cách sử dụng l-value từ tham số thực.
Cần phải rõ ràng rằng, như trong cách gọi theo giá trị, các câu hỏi
sau đây về thứ tự đánh giá phải được trả lời. Nếu có nhiều hơn một
tham số kết quả, thì nên thực hiện “phép gán lùi” tương ứng từ thực
tế sang chính thức theo thứ tự nào (ví dụ: từ trái sang phải)? Cuối
cùng, giá trị l của tham số thực được xác định khi nào? Hợp lý để xác
định nó cả khi hàm được gọi và khi nó kết thúc.
Có thể thấy rằng, trong quá trình thực thi body, không có mối liên hệ
nào giữa tham số chính thức và thực tế. Không có cách nào để sử
dụng tham số kết quả để chuyển thông tin từ người gọi đến bộ nhớ.
Ví dụ về cuộc gọi theo kết quả được hiển thị trong Hình 7.6. Việc
thực hiện cuộc gọi theo kết quả tương tự như gọi theo giá trị, nó chia
sẻ những ưu điểm và nhược điểm của nó. Từ quan điểm thực dụng,
chế độ theo kết quả đơn giản hóa việc thiết kế các hàm phải trả về
(kết quả là cung cấp) nhiều hơn một giá trị, mỗi giá trị trong một biến
khác nhau.

Call by value-result:
Sự kết hợp giữa gọi theo giá trị và gọi theo kết quả tạo ra một
phương thức được gọi là gọi theo giá trị-kết quả. Đây là một phương
pháp thực hiện giao tiếp hai chiều bằng cách sử dụng tham số chính
thức như một biến cục bộ cho thủ tục
Tham số thực tế phải là một biểu thức có thể mang lại l-value. Tại
cuộc gọi, tham số thực được đánh giá và giá trị r do đó thu được
được gán cho tham số chính thức. Khi kết thúc thủ tục, ngay trước
khi môi trường cục bộ bị phá hủy, giá trị hiện tại của tham số chính
thức được gán cho vị trí tương ứng với tham số thực. Ví dụ về cuộc
gọi theo giá trị-kết quả được hiển thị trong Hình 7.7
Tham số thực tế phải là một biểu thức có thể mang lại giá trị l. Tại cuộc gọi,
tham số thực được đánh giá và giá trị r do đó thu được được gán cho tham
số chính thức. Khi kết thúc thủ tục, ngay trước khi môi trường cục bộ bị phá
hủy, giá trị hiện tại của tham số chính thức được gán cho vị trí tương ứng với
tham số thực.
Một ví dụ về cuộc gọi theo giá trị-kết quả được hiển thị trong Hình 7.7.
Việc triển khai chính tắc của lệnh gọi theo giá trị-kết quả tương tự như cách
gọi theo giá trị, ngay cả khi một số ngôn ngữ (ví dụ: Ada) chọn triển khai nó
dưới dạng lệnh gọi bằng tham chiếu trong trường hợp dữ liệu có kích thước
lớn, do đó, các vấn đề của chi phí cuộc gọi theo giá trị có thể được tránh.

Tuy nhiên, việc triển khai lệnh gọi theo giá trị-kết quả bằng cách sử dụng
một tham chiếu không đúng về mặt ngữ nghĩa. Hãy xem xét một chút, đoạn
chương trình trong Hình 7.8. Ngay từ cái nhìn đầu tiên, lệnh có điều kiện có
trong phần thân của foo dường như vô dụng, vì x và y vừa nhận các giá trị
riêng biệt. Thực tế là x và y chỉ có các giá trị khác nhau khi không có alias.
Mặt khác, nếu x và y là hai tên khác nhau của cùng một biến thì rõ ràng điều
kiện x == y luôn đúng.
Do đó, nếu x và y được chuyển bằng giá trị-kết quả (ko có alias), thì lệnh gọi
foo (a, a, b) kết thúc mà giá trị của b không được sửa đổi.
Mặt khác, nếu x và y được truyền bằng tham chiếu (có alias), foo (a, a, b) kết
thúc bằng cách gán giá trị 1 cho b.
12/07/2021

Gọi theo tên


Gọi theo tên, được giới thiệu trong ALGOL60, là một cách truyền
tham số rõ ràng hơn về mặt ngữ nghĩa so với tham chiếu.
Nó không còn được sử dụng bởi bất kỳ ngôn ngữ mệnh lệnh chính
nào. Tuy nhiên, nó là một phương pháp quan trọng về mặt khái niệm
đáng để nỗ lực nghiên cứu chi tiết vì các tính chất và cách thực hiện
của nó.
Cho f là một hàm với một tham số hình thức duy nhất, x, và a là biểu
thức có kiểu tương thích với x. Một lệnh gọi đến f với một tham số
thực a tương đương về mặt ngữ nghĩa với việc thực thi phần thân của
f trong đó tất cả các lần xuất hiện của tham số chính thức, x, đã
được thay thế bằng a.

Có thể dễ dàng nhận thấy, đó là một quy tắc rất đơn giản. Nó làm
giảm ngữ nghĩa của lời gọi hàm đối với hoạt động cú pháp của việc
mở rộng phần thân của hàm sau khi thay thế bằng văn bản của thực
tế cho tham số hình thức. Tuy nhiên, khái niệm thay thế này không
phải là một khái niệm đơn giản vì nó phải tính đến thực tế là nó có
thể phải đối phó với một số biến khác nhau có cùng tên.
Ví dụ, hãy xem xét chức năng trong Hình 7.9. Nếu chúng ta áp dụng
quy tắc sao chép một cách mù quáng, lời gọi foo (x + 1) dẫn đến việc
thực hiện trả về x + x + 1. Lệnh này, được thực thi trong môi trường
cục bộ của foo, trả về giá trị 5. Nhưng rõ ràng đây là một ứng dụng
sai quy tắc sao chép vì nó làm cho kết quả của hàm phụ thuộc vào
tên của biến cục bộ.
Nếu phần thân của foo là:
{int z = 2; trả về z + y;}
cùng một lệnh gọi sẽ dẫn đến việc thực hiện trả về z + x + 1, với kết
quả là 3

Trong lần thay thế đầu tiên mà chúng tôi đề xuất, chúng tôi nói rằng
tham số thực, x, đã được khai báo cục bộ nắm bắt. Do đó, sự thay
thế mà quy tắc sao chép nói chuyện phải là một sự thay thế không
nắm bắt các biến. Không thể tránh được việc có các biến khác nhau
có cùng tên, vì vậy chúng ta có thể có được thay thế không lưu bằng
cách yêu cầu tham số hình thức, ngay cả sau khi thay thế, được đánh
giá trong môi trường của trình gọi chứ không phải trong môi trường
của callee.
Có nghĩa gặp nó được gọi trong hàm mới đánh giá -> chứ không đánh
giá ngay lúc truyền.
Lưu ý cách quy tắc sao chép yêu cầu tham số thực tế phải được đánh
giá mỗi khi gặp tham số chính thức trong quá trình thực thi.
Hãy xem xét ví dụ của Hình 7.10, trong đó cấu trúc i ++ có ngữ nghĩa
là trả về giá trị hiện tại của biến i và sau đó tăng giá trị của biến lên
1.
Quy tắc sao chép yêu cầu cấu trúc i ++ phải được đánh giá hai lần.
Một lần cho mỗi lần xuất hiện của tham số chính thức y trong fie. Lần
đầu tiên, đánh giá của nó trả về giá trị 2 và tăng giá trị của i lên một.
Lần thứ hai, nó trả về giá trị 3 và tăng i lần nữa.

Ví dụ chúng ta vừa thảo luận cho thấy việc coi call by name là một
cách phức tạp để mô tả call by value-result là một lỗi như thế nào.
Hai chế độ khác nhau về mặt ngữ nghĩa, như Hình 7.11 cho thấy.

Trước tiên, chúng ta hãy giả sử rằng chúng ta phải thực thi fiefoo với
các tham số được truyền bởi giá trị-kết quả. Khi kết thúc, chúng ta sẽ
có A [1] với giá trị 1 và i với giá trị 2, trong khi phần còn lại của mảng
A chưa được chạm vào.
Mặt khác, nếu chúng ta thực hiện cùng một thủ tục với hai tham số
được truyền theo tên, khi kết thúc, chúng ta sẽ có A [1] với giá trị 4,
i với giá trị 2 và điều quan trọng hơn là phần tử A [2 ] sẽ được cập
nhật lên giá trị 1. Có thể thấy rằng, trong trường hợp này, phép
value-result và lệnh gọi bằng tham chiếu sẽ thể hiện cùng một hành
vi.

Nó vẫn còn để mô tả cách gọi theo tên có thể được thực hiện. Chúng
ta đã thảo luận về sự cần thiết đối với người gọi để truyền không chỉ
biểu thức văn bản tạo thành tham số thực tế của nó mà còn cả môi
trường mà biểu thức này phải được đánh giá. Chúng tôi gọi một cặp,
(biểu thức, môi trường), trong đó môi trường bao gồm (ít nhất) tất
cả các biến tự do trong biểu thức là một bao đóng.
Trong trường hợp này, bao đóng là một cặp được hình thành từ hai
con trỏ: con trỏ đầu tiên trỏ đến mã đánh giá biểu thức của tham số
hình thức, con trỏ thứ hai là con trỏ đến chuỗi tĩnh, chỉ ra khối tạo
thành môi trường cục bộ trong đó để đánh giá biểu thức.
Khi một thủ tục f với tham số tên chính thức, x, được gọi với tham số
thực a, thì lệnh gọi tạo ra một bao đóng có thành phần đầu tiên là
con trỏ đến mã cho a và có thành phần thứ hai là con trỏ tới bản ghi
kích hoạt thực tế của (người gọi) . Sau đó, nó ràng buộc sự bao đóng
này.
Ví dụ: sử dụng một con trỏ khác) đến tham số chính thức x nằm trong
bản ghi kích hoạt của thủ tục được gọi
Cuối cùng chúng ta có thể tóm tắt những gì chúng ta biết khi gọi tên.
Nó là một phương thức hỗ trợ các tham số đầu vào và đầu ra. Tham
số chính thức không tương ứng với một biến cục bộ của thủ tục;
tham số thực có thể là một biểu thức tùy ý. Có thể là các tham số
thực tế và chính thức có thể được bí danh. Tham số thực tế phải
đánh giá thành giá trị l nếu tham số chính thức xuất hiện ở bên trái
của phép gán. Ngữ nghĩa của lệnh gọi theo tên được thiết lập bởi quy
tắc sao chép cho phép duy trì liên kết không đổi giữa các tham số
chính thức và thực tế trong quá trình thực thi. Việc triển khai chính
tắc sử dụng một bao đóng. Môi trường cục bộ của thủ tục được mở
rộng với sự liên kết giữa chính thức và bao đóng, môi trường sau bao
gồm tham số thực và môi trường mà lệnh gọi xảy ra.

Đây là một phương pháp truyền tham số rất tốn kém, cả vì sự cần
thiết phải truyền một cấu trúc phức tạp và đặc biệt, vì đánh giá lặp
lại tham số thực trong một môi trường không phải là môi trường hiện
tại.
Gọi theo tên cho phép các tác dụng phụ được khai thác để có được
mã thanh lịch và nhỏ gọn, mặc dù nó thường dẫn đến mã khó hiểu và
khó bảo trì.
Đây là trường hợp của cái gọi là Thiết bị của Jensen, sử dụng tên
truyền để chuyển một biểu thức phức tạp và đồng thời, một biến xuất
hiện trong cùng một biểu thức, theo cách mà các sửa đổi đối với biến
thay đổi giá trị của biểu thức.

Tác dụng phụ của việc truyền một tham số theo tên là, trong phần
thân của vòng lặp trong tính tổng, giá trị của exp có thể phụ thuộc
vào giá trị của i. Phản ánh của khoảnh khắc cho thấy rằng lệnh gọi
trên dòng cuối cùng tương đương với phép tính tổng:

Jensen’s Device cho phép gọi theo tên được sử dụng như một cách
để lấy ra các thủ tục “bậc cao hơn” mạnh mẽ và đặc biệt tại thời
điểm gọi (trong trường hợp của ví dụ, bằng cách chỉ ra biểu thức để
tính tổng).
7.2 Higher-Order Functions
Hàm bậc cao khi nó chấp nhận làm tham số hoặc trả về một hàm khác
là kết quả của nó. Mặc dù không có sự thống nhất nhất trí trong tài
liệu, chúng tôi sẽ nói rằng một ngôn ngữ lập trình là Hàm bậc cao khi
nó cho phép các hàm dưới dạng tham số hoặc là kết quả của các hàm
khác.
Các ngôn ngữ có hàm là tham số khá phổ biến. Mặt khác, các ngôn
ngữ cho phép các hàm trả về các hàm do đó ít phổ biến hơn. Tuy
nhiên, kiểu hoạt động cuối cùng này là một trong những cơ chế cơ
bản của ngôn ngữ lập trình hàm (chúng ta sẽ đề cập sâu hơn trong
Chương 11)
7.2.1 Functions as Parameters
Trường hợp chung mà chúng tôi muốn phân tích là ngôn ngữ có các
tham số hàm, môi trường lồng nhau và khả năng xác định các hàm ở
mọi cấp độ lồng nhau.
Hai điểm chính của ví dụ là:
(i) thực tế là f được truyền dưới dạng tham số thực tế cho g và sau
đó được gọi thông qua tham số chính thức h;
(ii) tên x được xác định nhiều lần, vì vậy cần phải xác định đâu là môi
trường (non_local) trong đó f sẽ được đánh giá

Liên quan đến câu hỏi thứ hai này, người đọc sẽ không ngạc nhiên
nếu chúng ta quan sát thấy rằng có hai khả năng lựa chọn môi trường
phi địa phương để sử dụng khi thực thi một hàm f được gọi bằng
tham số chính thức h:

• Sử dụng môi trường đang hoạt động tại thời điểm tạo liên kết giữa
h và f (xảy ra trên dòng 11). Trong trường hợp này, chúng tôi nói
rằng ngôn ngữ sử dụng một chính sách deep binding.
• Sử dụng môi trường đang hoạt động khi cuộc gọi của f sử dụng h
xảy ra (xảy ra trên dòng 7). Trong trường hợp này, chúng tôi nói rằng
ngôn ngữ sử dụng chính sacsh shallow binding.

Tất cả các ngôn ngữ phổ biến sử dụng phạm vi tĩnh cũng sử dụng
deep binding (vì việc lựa chọn một chính sách shallow có vẻ mâu
thuẫn ở cấp độ phương pháp luận). Vấn đề không rõ ràng đối với các
ngôn ngữ có dynamic scope, trong số đó có những ngôn ngữ có ràng
deep cũng như shallow.
Trong static scope và deep binding, lệnh gọi h (3) trả về 4 (và g trả
về 6). X trong phần thân của f khi nó được gọi bằng cách sử dụng h là
một trong khối ngoài cùng;
Trong dynamic scope và deep binding, lệnh gọi h (3) trả về 7 (và g trả
về 9). X trong phần thân của f khi nó được gọi bằng cách sử dụng h là
một cục bộ của khối mà lệnh gọi g (f) xảy ra;
Trong dynamic scope và shallow binding, lệnh gọi h (3) trả về 5 (và g
trả về 7). X trong phần thân của f tại thời điểm gọi của nó thông qua
h là một địa phương của g.

Xem thêm về bài tập 6.


Thực hiện liên kết sâu: Ràng buộc nông không gây ra các vấn đề triển
khai bổ sung đối với kỹ thuật được sử dụng để triển khai phạm vi
động. Ít nhất về mặt khái niệm là đủ để tìm kiếm liên kết cuối cùng
của mọi tên trong môi trường. Tuy nhiên, mọi thứ lại khác, đối với
liên kết sâu, đòi hỏi cấu trúc dữ liệu phụ trợ ngoài các chuỗi tĩnh và
động thông thường

Để sửa chữa ý tưởng của chúng tôi, chúng ta hãy xem xét trường hợp
của một ngôn ngữ có phạm vi tĩnh và liên kết sâu (trường hợp phạm
vi động được để cho người đọc, xem Bài tập 6). Từ Sect. 5.5.1, chúng
ta đã biết rằng đối với bất kỳ lệnh gọi trực tiếp nào (không phải là
lệnh gọi tham số chính thức) của hàm f, có thông tin liên kết tĩnh
(một số nguyên) thể hiện mức lồng ghép của định nghĩa f với đối với
khối mà cuộc gọi xảy ra. Thông tin này được sử dụng động bởi máy
trừu tượng để khởi tạo con trỏ chuỗi tĩnh (nghĩa là môi trường phi địa
phương) trong bản ghi kích hoạt cho f. Mặt khác, khi một hàm f được
gọi bằng tham số chính thức, h, thì không có thông tin nào có thể
được liên kết với lệnh gọi bởi vì nó được gọi thông qua một tham số
chính thức. Thật vậy, trong quá trình kích hoạt khác nhau của thủ tục
mà nó nằm trong đó, hình thức có thể được liên kết với các chức
năng khác nhau (ví dụ như trường hợp này với lệnh gọi h (3) trong
Hình 7.13)

Nói cách khác, rõ ràng là với ràng buộc sâu, thông tin về con trỏ
chuỗi tĩnh phải được xác định tại thời điểm kết hợp giữa các tham số
chính thức và thực tế được tạo ra. Với h chính thức phải được liên
kết không chỉ với mã của f mà còn với môi trường phi địa phương
trong đó phần thân của f sẽ được đánh giá. Một môi trường phi địa
phương như vậy có thể được xác định theo cách đơn giản: tương ứng
với lệnh gọi có dạng g (f) (thủ tục g được gọi với tham số thực của
hàm f), chúng ta có thể kết hợp tĩnh với tham số f thông tin về lồng
mức xác định của f trong khối mà lệnh gọi g (f) xảy ra. Khi lệnh gọi
này được thực hiện, máy trừu tượng sẽ sử dụng thông tin này để kết
hợp với tham số chính thức tương ứng với f, cả mã cho f và một con
trỏ tới bản ghi kích hoạt của khối bên trong mà f được khai báo (con
trỏ này được xác định bằng cách sử dụng cùng các quy tắc đã được
thảo luận trong Sect. 5.5.1).
Khi tham số chính thức được sử dụng để gọi một hàm (chưa được
biết về mặt tĩnh), máy trừu tượng sẽ tìm thấy trong thành phần đầu
tiên của cặp mã để chuyển điều khiển và gán nội dung của thành
phần thứ hai của bao đóng cho chuỗi tĩnh con trỏ của bản ghi kích
hoạt cho lời gọi mới.
Do đó, thành phần thứ hai của quá trình đóng cửa được xác định
bằng 1 bước dọc theo chuỗi tĩnh (thu được một con trỏ đến khối bên
ngoài). Khi f được gọi thông qua tên h, bản ghi kích hoạt tương ứng
được đẩy lên ngăn xếp. Giá trị của con trỏ chuỗi tĩnh được lấy từ
thành phần thứ hai của bao đóng.

các vấn đề chúng ta đang thảo luận chỉ xuất hiện khi ngôn ngữ cho
phép định nghĩa các hàm với môi trường phi địa phương, tức là cho
phép định nghĩa các hàm bên trong các khối lồng nhau.
Để truyền một hàm dưới dạng tham số, chỉ cần truyền một con trỏ tới
mã của nó là đủ. Tất cả các tham chiếu phi địa phương trong phần
thân của hàm sẽ được giải quyết trong môi trường chung.
Binding policy and static scope
Chúng tôi đã quan sát cách tất cả các ngôn ngữ có phạm vi tĩnh sử
dụng liên kết sâu. Ngay từ cái nhìn đầu tiên, có vẻ như ràng buộc sâu
hay nông không có gì khác biệt trong trường hợp phạm vi tĩnh. Rốt
cuộc, môi trường phi địa phương của một hàm được xác định từ vị trí
(tĩnh) của phần khai báo của nó chứ không phải theo cách mà nó
được gọi.
 Static scope
nó là quy tắc phạm vi (và không ràng buộc) thiết lập rằng mọi lệnh
gọi f (dù trực tiếp, sử dụng tên của nó hay gián tiếp, sử dụng tham số
chính thức) đều được đánh giá trong môi trường phi địa phương
ngoài cùng.

Nói chung, tuy nhiên, nó không phải như thế này. Lý do cho điều này
là có thể có nhiều bản ghi kích hoạt cho cùng một hàm hiện diện đồng
thời trên ngăn xếp (điều này rõ ràng xảy ra khi chúng ta có các hàm
đệ quy hoặc đệ quy lẫn nhau). Nếu một thủ tục được truyền từ một
trong những kích hoạt này, có thể tạo ra tình huống trong đó chỉ
riêng các quy tắc phạm vi là không đủ để xác định môi trường phi địa
phương nào sẽ sử dụng để gọi tham số chức năng
Trọng tâm của vấn đề là tham chiếu (phi địa phương) đến x bên trong
fie. Các quy tắc phạm vi cho chúng ta biết rằng một x như vậy đề cập
đến tham số chính thức đến foo (khi nó xảy ra, là x duy nhất được
khai báo trong chương trình). Nhưng khi fie được gọi (sử dụng f chính
thức), có hai trường hợp hoạt động của foo (và do đó hai trường hợp
của môi trường cục bộ của nó)
Lần kích hoạt đầu tiên từ lệnh gọi foo (g, 1), trong đó x được liên kết
(với một vị trí chứa) giá trị 1 và lần kích hoạt thứ hai từ lệnh gọi (đệ
quy) foo (fie, 0), trong đó x là được liên kết với giá trị 0. Bên trong
lần kích hoạt thứ hai này, lệnh gọi đến fie thông qua f được thực
hiện. Các quy tắc phạm vi không nói gì về trường hợp nào của x nên
được sử dụng trong phần thân của f. Chính tại thời điểm này, chính
sách ràng buộc sẽ can thiệp. Sử dụng liên kết sâu, môi trường được
thiết lập khi tạo mối liên kết giữa fie và f, đó là khi x được liên kết
với giá trị 1. Do đó, biến z sẽ được gán giá trị 1.
Cuối cùng chúng tôi có thể hoàn thành các thành phần góp phần xác
định chính xác môi trường đánh giá cho một ngôn ngữ có cấu trúc
khối. Những điều sau đây là cần thiết:
• Quy tắc hiển thị, thường được đảm bảo bởi cấu trúc khối.
• Các ngoại lệ đối với các quy tắc hiển thị (có tính đến, ví dụ, định
nghĩa lại tên và khả năng hoặc không sử dụng tên trước khi khai báo).
• Quy tắc phạm vi.
• Các quy tắc cho phương thức truyền tham số.
• Chính sách ràng buộc
7.2.2 Functions as Results
Khả năng tạo ra các chức năng như là kết quả của các chức năng
khác cho phép tạo ra các chức năng động trong thời gian chạy. Rõ
ràng là nói chung, một hàm được trả về là kết quả không thể được
biểu diễn tại thời điểm thực thi chỉ bằng mã của nó, nó cũng sẽ cần
thiết phải bao gồm môi trường mà hàm sẽ được đánh giá.

Chúng ta hãy xem xét một ví dụ đơn giản đầu tiên trong Hình 7.17.
Trước hết, chúng ta hãy sửa chữa ký hiệu: bởi void-> int, chúng ta
biểu thị kiểu của các hàm không nhận đối số (void) và trả về giá trị
kiểu số nguyên (int). Do đó, dòng thứ hai của đoạn mã là khai báo
của một hàm F, hàm này trả về một hàm không có đối số, hàm này
cũng trả về một số nguyên (lưu ý rằng hàm trả về g trả về “hàm”,
không phải ứng dụng của nó). Dòng đầu tiên sau phần thân của F là
phần khai báo tên gg mà kết quả đánh giá F được liên kết động
Sẽ không khó để thuyết phục chúng ta rằng hàm gg trả về giá trị kế
thừa của x. Sử dụng chế độ phạm vi tĩnh, x này được cố định bởi cấu
trúc của chương trình chứ không phải bởi vị trí của lệnh gọi gg, có
thể xuất hiện trong môi trường mà ở đó xuất hiện một định nghĩa
khác về tên x.
Do đó, chúng ta có thể nói rằng, nói chung, khi một hàm trả về kết
quả là một hàm, thì kết quả này là một bao đóng. Do đó, máy trừu
tượng phải được sửa đổi một cách thích hợp để tính đến các lệnh gọi
đến các lệnh đóng của tài khoản. Tương tự với những gì xảy ra khi
một hàm được gọi thông qua một tham số chính thức, khi một hàm
có giá trị được nhận động (như gg), thì con trỏ chuỗi tĩnh của bản ghi
kích hoạt của nó được xác định bằng cách sử dụng bao đóng được
liên kết (chứ không phải các quy tắc chính tắc được thảo luận trong
Phần 5.5.1 , sẽ không có ích gì)
Tình hình chung, hơn nữa, phức tạp hơn nhiều. Nếu có thể trả về một
hàm từ bên trong một khối lồng nhau, có thể sắp xếp rằng môi
trường đánh giá của nó sẽ tham chiếu đến một tên mà theo nguyên
tắc ngăn xếp, nó sẽ bị hủy.

Khi kết quả của F () được gán cho gg, bao đóng tạo thành giá trị của
nó trỏ đến một môi trường chứa tên x. Nhưng môi trường này là cục
bộ đối với F và do đó sẽ bị phá hủy khi nó kết thúc. Làm thế nào để
có thể gọi gg muộn hơn giá trị này mà không tạo ra một tham chiếu
lơ lửng đến x? Câu trả lời chỉ có thể là quyết liệt: từ bỏ kỷ luật ngăn
xếp cho các bản ghi kích hoạt, để sau đó chúng có thể tồn tại vô thời
hạn, bởi vì chúng có thể tạo thành môi trường cho các chức năng sẽ
được đánh giá sau đó. Trong các ngôn ngữ có các đặc điểm mà chúng
ta vừa thảo luận, môi trường cục bộ có thời gian tồn tại không giới
hạn.
Giải pháp phổ biến nhất trong trường hợp này là phân bổ tất cả các
bản ghi kích hoạt trong heap và để nó cho một bộ thu gom rác để
phân bổ chúng khi phát hiện ra rằng không có tham chiếu đến tên mà
chúng chứa.
Mọi ngôn ngữ chức năng đều được xây dựng xung quanh khả năng trả
về các chức năng dưới dạng kết quả. Do đó, họ phải đối đầu với vấn
đề này. Ngược lại, việc trả về các hàm dưới dạng kết quả trong ngôn
ngữ mệnh lệnh là rất hiếm; điều này hoàn toàn là để duy trì kỷ luật
ngăn xếp cho các bản ghi kích hoạt. Trong các ngôn ngữ mệnh lệnh
cho phép các hàm làm kết quả, nói chung, có nhiều hạn chế nhằm
đảm bảo rằng không bao giờ có thể tạo một tham chiếu đến một môi
trường đã ngừng hoạt động (ví dụ: không có hàm lồng nhau (C, C ++),
trả về chỉ các hàm không lồng nhau (Modula-2, Modula-3), hạn chế
một cách thích hợp phạm vi của các hàm lồng nhau được trả về
(Ada), v.v.).

You might also like