Các thủ tục như là các trừu tượng Black-box. Procedures as Black-Box Abstractions

Sqrt là một ví dụ về quá trình đã được định nghĩa bởi một set các procedures đã được định nghĩa trước. Chú ý rắng, việc định nghĩa sqrt-iter là một công thức đệ quy; procedure được định nghĩa trong body của chính nó. Ý tưởng về viecj định nghĩa một procedure trong các điều kiện ràng buộc của chính nó có lẽ sẽ rất phức tạp và khó hiểu. Nhưng hãy để ý một số điểm quan trọng được minh chứng trong ví dụ căn bậc hai sqrt của một số.

Bài toán tính căn bậc hai được chia nhỏ thành các bài toán con:

làm thế nào để biết liệu một kết quả dự đoán có là kết quả mong đợi? làm thế nào để cải thiện giá trị của guess?, v.v. Mỗi bài toán con được thực hiện bằng một procedure riêng biệt. Toàn bộ chương trình sqrt có thể được xem như một cụm – set- cluster các procedures (được hiển thị trong hình trên) phản ánh việc phân rã vấn đề thành các vấn đề con.

Tầm quan trọng của chiến lược tách vấn đề không chỉ đơn giản là chia chương trình lớn thành nhiều phần con. Sau tất cả, bất kì ứng dụng lớn nào cũng có thể chia nó thành nhiều phần con—mười dòng đầu tiên, mười dòng tiếp theo, mười dòng tiếp theo, và cứ thế tiếp tục v.v. Hơn nữa, điều quan trọng là mỗi procedure phải hoàn thành một nhiệm vụ có thể xác định được và có thể được sử dụng như một mô-đun trong việc xác định các thủ tục khác.

Ví dụ, khi chúng ta định nghĩa thế nào là good-enough? về mặt bình phương, chúng ta có thể coi procedure bình phương như một “black box”. Chúng ta không quan tâm đến việc làm thế nào
procedure tính toán kết quả của nó, chỉ cần biết là thủ tục đó tính giá trị bình phương của bất kỳ thứ gì. Các chi tiết về cách tính bình phương có thể bị loại bỏ, sẽ được xem xét vào lúc khác.

Thật vậy, như thủ tục good-enough? Bình phương không hẳn là một thủ tục mà là một sự trừu tượng hóa của một thủ tục, nó được gọi là sự trừu tượng hóa thủ tục. Ở mức độ trừu tượng này, mọi thủ tục tính bình phương đều hiệu quả.

Do đó, chỉ xem xét các giá trị mà chúng trả về, hai procedure dưới đây đều tính giá trị bình phương của một số mà không thể phân biệt được. Mỗi thủ tục đều truyền vào một argument và tiến hành tính toán bình phương của đối số đó.

(define (square x) (* x x))
(define (square x) (exp (double (log x))))
(define (double x) (+ x x))

Vì vậy, việc định nghĩa procedure nên loại bỏ chi tiêt procedure của thủ tục. Người dùng
không cần phải quan tâm đến quá trình thực thi procedure để sử dụng nó.

Local names

Chi tiết về việc thực hiện một thủ tục không quan trọng đối với
người sử dụng thủ tục. Vì vậy, các thủ tục sau đây sẽ
không thể phân biệt được:

(define (square x) (* x x))
(define (square y) (* y y))

Nguyên tắc này – ý nghĩa của một thủ tục phải độc lập
tên các parameter được sử dụng trong thủ tục. Kết quả là tên tham số của một thủ tục phải là tên cục bộ của <body> của thủ tục. Ví dụ: bình phương trong định nghĩa
thủ tục good-enough? trong thủ tục căn bậc hai :

Thủ tục trên có ý định muốn làm gì? là để xác định xem bình phương của guess
của đối số thứ nhất nằm trong phạm vi sai số nhất định với đối số thứ hai.
Sử dụng tên guess đề cập đến đối số thứ nhất và x để chỉ đối số thứ hai. Đối số của thủ tục square là guess. Khởi tạo thủ tục square không được ảnh hưởng
giá trị của x được sử dụng bởi good-enough?, Bởi vì giá trị đó của x
có thể cần thiết good-enough? sau khi square được tính toán xong.

Trong định nghĩa thủ tục good-enough ở trên? biến guess và x bị các biến có giá trị nhưng <, -, abs và square đều tự do trong môi trường. Ý nghĩa của good-enough? phải độc lập với tên chúng ta đã chọn cho guess và x miễn là chúng khác biệt và khác với <, -, abs và square. (Nếu chúng ta đổi tên guess thành abs chúng ta sẽ gây ra lỗi khi sử dụng abs).
Nó chắc chắn phụ thuộc vào thực tế (ngoài định nghĩa này) rằng ký hiệu abs đặt tên cho một thủ tục tính giá trị tuyệt đối của một số. good-enough? sẽ tính một hàm khác nếu chúng ta thay cos bằng abs trong định nghĩa của nó.

Định nghĩa cục bộ và cấu trúc khối.

Chương trình tính căn bậc hai bao gồm các thủ tục tách biệt rời nhau

(define (sqrt x) (sqrt-iter 1.0 x))
(define (sqrt-iter guess x) (if (good-enough? guess x) guess (sqrt-iter (improve guess x) x)))

(define (good-enough? guess x) (< (abs (- (square guess) x)) 0.001))

(define (improve guess x) (average guess (/ x guess)))

Procedure quan trọng nhất đối với người dùng muốn gọi thủ tục căn bậc hai là sqrt. Các thủ tục khác (sqrt-iter, good-enough?, và improve) chỉ làm phức tạp hơn thêm. Chúng có thể không định nghĩa
bất kỳ thủ tục nào khác được gọi là đủ tốt? như một phần của một chương trình khác để làm việc cùng với chương trình căn bậc hai, vì sqrt cần các thủ tục đó.
Vấn đề đặc biệt nghiêm trọng trong việc xây dựng các hệ thống lớn bởi nhiều lập trình viên riêng biệt. Ví dụ, trong việc xây dựng một library lớn với nhiều thủ tục khác nhau, nhiều hàm số được
được tính toán dưới dạng các phép tính gần đúng liên tiếp và có lẽ sẽ có các thủ tục được đặt tên là good-enough? và improve như các procedure hỗ trợ.

Chúng ta cần phải lồng các procedure con vào procedure cha, ẩn chúng bên trong sqrt để
sqrt đó có thể cùng tồn tại với các phép tính gần đúng liên tiếp khác, mỗi phép tính có thủ tục good-enough? riêng của chính nó. Để làm được điều này, chúng ta cho phép một thủ tục có các định nghĩa cục bộ đối với thủ tục đó.

Ví dụ, trong bài toán căn bậc hai chúng ta có thể viết

Việc lồng ghép các định nghĩa như vậy, được gọi là cấu trúc khối, về cơ bản là giải pháp tốt cho vấn đề đóng gói tên đơn giản nhất. Nhưng có một ý tưởng hay hơn được đưa ra. Ngoài việc định nghĩa các procedure cục bộ về các thủ tục phụ trợ, chúng ta có thể đơn giản hóa chúng.

Vì x bị ràng buộc trong định nghĩa của sqrt, nên các procedure good-enough?, improve và sqrt-iter,
được xác định nội bộ trong sqrt, nằm trong phạm vi của x. Hơn nữa, chúng ta không cần thiết phải truyền x một cụ thể cho từng thủ tục. Thay vì, chúng ta cho phép x là một biến tự do trong các định nghĩa nội bộ, như được hiển thị bên dưới. X nhận giá trị của nó từ đối số khi
procedure sqrt được gọi. Kỷ thuật này được gọi là lexical scoping.

Chúng ta sẽ sử dụng cấu trúc khối một cách rộng rãi để giúp chúng ta chia các chương trình lớn thành các phần dễ theo dõi và kiểm soát hơn. Ý tưởng về cấu trúc khối (block structure) bắt nguồn từ
ngôn ngữ lập trình Algol 60. Nó xuất hiện trong hầu hết các ngôn ngữ lập trình tiên tiến và là một công cụ quan trọng để giúp tổ chức việc xây dựng các chương trình lớn.

Trả lời

Email của bạn sẽ không được hiển thị công khai. Các trường bắt buộc được đánh dấu *