Các ngôn ngữ mệnh lệnh được sử dụng hiệu quả trong lập trình do người lập trình có thể tác động trực tiếp vào phần cứng.. Sau đây là một số đặc trưng của ngôn ngữ lập trình mệnh lệnh : −
Trang 1TS PHAN HUY KHÁNH
Lập trình hàm
NHÀ XUẤT BẢN KHOA HỌC VÀ KỸ THUẬT
Trang 3i
Mục lục
CHƯƠNG I NGUYÊN LÝ LẬP TRÌNH HÀM 1
I.1 Mở đầu về ngôn ngữ lập trình 1
I.1.1 Vài nét về lịch sử 1
I.1.2 Định nghĩa một ngôn ngữ lập trình 2
I.1.3 Khái niệm về chương trình dịch 4
I.1.4 Phân loại các ngôn ngữ lập trình 5
I.1.5 Ngôn ngữ lập trình mệnh lệnh 7
I.2 Cơ sở của các ngôn ngữ hàm 8
I.2.1 Tính khai báo của các ngôn ngữ hàm 8
I.2.2 Định nghĩa hàm 11
I.2.3 Danh sách 13
I.2.4 Phép so khớp 16
I.2.5 Phương pháp currying (tham đối hoá từng phần) 17
I.2.6 Khái niệm về bậc của hàm 18
I.2.7 Kiểu và tính đa kiểu 20
I.2.8 Tính hàm theo kiểu khôn ngoan 22
I.2.9 Một số ví dụ 25
1 Loại bỏ những phần tử trùng nhau 25
2 Sắp xếp nhanh quicksort 25
3 Bài toán tám quân hậu 26
4 Bài toán hamming 27
I.3 Kết luận 29
CHƯƠNG II NGÔN NGỮ SCHEME 33
II.1 Giới thiệu Scheme 33
II.2 Các kiểu dữ liệu của Scheme 34
II.2.1 Các kiểu dữ liệu đơn giản 34
II.2.1.1 Kiểu số 34
II.2.1.2 Kiểu lôgích và vị từ 36
II.2.1.3 Ký hiệu 38
II.2.2 Khái niệm về các biểu thức tiền tố 39
II.2.3 S-biểu thức 41
II.3 Các định nghĩa trong Scheme 41
II.3.1 Định nghĩa biến 41
II.3.2 Định nghĩa hàm 42
II.3.2.1 Khái niệm hàm trong Scheme 42
II.3.2.2 Gọi hàm sau khi định nghĩa 43
II.3.2.3 Sử dụng các hàm bổ trợ 44
II.3.2.4 Tính không định kiểu của Scheme 45
Trang 4II.3.3 Cấu trúc điều khiển 45
II.3.3.1 Dạng điều kiện if 45
II.3.3.2 Biến cục bộ 47
1 Định nghĩa biến cục bộ nhờ dạng let 47
2 Phạm vi tự động của dạng let 48
3 Liên kết biến theo dãy : dạng let* 48
II.3.3.3 Định nghĩa các vị từ 49
II.3.4 Sơ đồ đệ quy và sơ đồ lặp 50
II.3.4.1 Sơ đồ đệ quy 50
II.3.4.2 Ví dụ 51
1 Tính tổng bình phương các số từ 1 đến n 51
2 Tính giai thừa 51
3 Hàm fibonacci 51
4 Tính các hệ số nhị thức 52
II.3.4.3 Tính dừng của lời gọi đệ quy 52
II.3.4.4 Chứng minh tính dừng 54
II.3.4.5 Sơ đồ lặp 54
II.3.5 Vào/ra dữ liệu 56
1 Đọc vào dữ liệu : read 56
2 In ra dữ liệu : write và display 56
3 Xây dựng vòng lặp có menu 57
CHƯƠNG III KIỂU DỮ LIỆU PHỨC HỢP 61
III.1 Kiểu chuỗi 61
III.2 Kiểu dữ liệu vectơ 64
III.3 Kiểu dữ liệu bộ đôi 64
III.3.1 Khái niệm trừu tượng hoá dữ liệu 64
III.3.2 Định nghĩa bộ đôi 66
III.3.3 Đột biến trên các bộ đôi 68
III.3.4 Ứng dụng bộ đôi 69
1 Biểu diễn các số hữu tỷ 69
2 Biểu diễn hình chữ nhật phẳng 72
III.4 Kiểu dữ liệu danh sách 74
III.4.1 Khái niệm danh sách 74
III.4.2 Ứng dụng danh sách 76
III.4.2.1 Các phép toán cơ bản cons, list, car và cdr 76
III.4.2.2 Các hàm xử lý danh sách 79
1 Các hàm length, append và reverse 79
2 Các hàm tham chiếu danh sách 80
3 Các hàm chuyển đổi kiểu 81
4 Các hàm so sánh danh sách 83
III.4.2.3 Dạng case xử lý danh sách 84
III.4.2.4 Kỹ thuật đệ quy xử lý danh sách phẳng 86
1 Tính tổng các phần tử của một danh sách 86
2 Danh sách các số nguyên từ 0 đến n 86
3 Nghịch đảo một danh sách 87
4 Hàm append có hai tham đối 87
5 Loại bỏ các phần tử khỏi danh sách 87
6 Bài toán tính tổng con 88
Trang 57 Lập danh sách các số nguyên tố 88
III.4.2.5 Kỹ thuật đệ quy xử lý danh sách bất kỳ 89
1 Làm phẳng một danh sách 89
2 Tính tổng các số có mặt trong danh sách 90
3 Loại bỏ khỏi danh sách một phần tử ở các mức khác nhau 90
4 Nghịch đảo danh sách 90
5 So sánh bằng nhau 91
III.4.3 Biểu diễn danh sách 92
III.4.3.1 Biểu diễn danh sách bởi kiểu bộ đôi 92
III.4.3.2 Danh sách kết hợp 96
1 Khái niệm danh sách kết hợp 96
2 Sử dụng danh sách kết hợp 97
III.4.3.3 Dạng quasiquote 98
III.4.4 Một số ví dụ ứng dụng danh sách 99
1 Tìm phần tử cuối cùng của danh sách 99
2 Liệt kê các vị trí một ký hiệu có trong danh sách 100
3 Tìm tổng con lớn nhất trong một vector 100
4 Bài toán sắp xếp dãy viên bi ba màu 101
5 Sắp xếp nhanh quicksort 102
CHƯƠNG IV KỸ THUẬT XỬ LÝ HÀM 107
IV.1 Sử dụng hàm 107
IV.1.1 Dùng tên hàm làm tham đối 107
IV.1.2 Áp dụng hàm cho các phần tử của danh sách 110
IV.1.3 Kết quả trả về là hàm 112
IV.2 Phep tính lambda 113
IV.2.1 Giới thiệu phép tính lambda 113
IV.2.2 Biễu diễn biểu thức lambda trong Scheme 114
IV.2.3 Định nghĩa hàm nhờ lambda 115
IV.2.4 Kỹ thuật sử dụng phối hợp lambda 117
IV.2.5 Định nghĩa hàm nhờ tích luỹ kết quả 120
1 Tính tổng giá trị của một hàm áp dụng cho các phần tử danh sách 120
2 Tính tích giá trị của một hàm áp dụng cho các phần tử danh sách 120
3 Định nghĩa lại hàm append ghép hai danh sách 120
4 Định nghĩa lại hàm map cho hàm một biến h 120
5 Định nghĩa các hàm fold 122
IV.2.6 Tham đối hoá từng phần 122
IV.2.7 Định nghĩa đệ quy cục bộ 123
IV.3 Xử lý trên các hàm 125
IV.3.1 Xây dựng các phép lặp 125
1 Hàm append-map 125
2 Hàm map-select 126
3 Các hàm every và some 126
IV.3.2 Trao đổi thông điệp giữa các hàm 127
IV.3.3 Tổ hợp các hàm 129
IV.3.4 Các hàm có số lượng tham đối bất kỳ 130
IV.4 Một số ví dụ 132
IV.4.1 Phương pháp xấp xỉ liên tiếp 132
IV.4.2 Tạo thủ tục định dạng 133
Trang 6IV.4.3 Xử lý đa thức 134
IV.4.3.1 Định nghĩa đa thức 134
IV.4.3.2 Biễu diễn đa thức 134
IV.4.3.3 Xử lý đa thức 135
1 Nhân đa thức với một hằng số 135
2 So sánh hai đa thức 136
3 Phép cộng đa thức 136
4 Phép nhân hai đa thức 137
IV.4.3.4 Biễu diễn trong một đa thức 137
IV.4.3.5 Đưa ra đa thức 138
IV.4.4 Thuật toán quay lui 139
IV.4.4.1 Bài toán tám quân hậu 139
IV.4.4.2 Tìm kiếm các lời giải 140
IV.4.4.3 Tổ chức các lời giải 143
CHƯƠNG V CẤU TRÚC DỮ LIỆU 147
V.1 Tập hợp 147
1 Phép hợp trên các tập hợp 148
2 Phép giao trên các tập hợp 149
3 Phép hiệu của hai tập hợp 149
4 Tìm các tập hợp con của một tập hợp 150
V.2 Ngăn xếp 150
V.2.1 Kiểu dữ liệu trừu tượng ngăn xếp 150
V.2.2 Xây dựng ngăn xếp 151
V.2.3 Xây dựng trình soạn thảo văn bản 152
V.2.4 Ngăn xếp đột biến 153
V.2.5 Tính biểu thức số học dạng hậu tố 156
V.3 Tệp 158
V.3.1 Cấu trúc dữ liệu trừu tượng kiểu tệp 158
V.3.2 Ví dụ áp dụng tệp 159
V.3.3 Tệp đột biến 160
V.4 Cây 162
V.4.1 Cây nhị phân 163
V.4.1.1 Kiểu trừu tượng cây nhị phân 163
V.4.1.2 Biểu diễn cây nhị phân 164
1 Biểu diễn tiết kiệm sử dụng hai phép cons 164
2 Biểu diễn dạng đầy đủ 165
3 Biểu diễn đơn giản 165
V.4.1.3 Một số ví dụ lập trình đơn giản 166
1 Đếm số lượng các nút có trong một cây 166
2 Tính độ cao của một cây 166
V.4.1.4 Duyệt cây nhị phân 167
V.4.2 Cấu trúc cây tổng quát 169
V.4.2.1 Kiểu trừu tượng cây tổng quát 169
V.4.2.2 Biểu diễn cây tổng quát 169
1 Biểu diễn cây nhờ một bộ đôi 169
2 Biểu diễn cây đơn giản qua các lá 170
V.4.2.3 Một số ví dụ về cây tổng quát 170
Trang 71 Đếm số lượng các nút trong cây 170
2 Tính độ cao của cây 171
V.4.2.4 Duyệt cây tổng quát không có xử lý trung tố 171
V.4.3 Ứng dụng cây tổng quát 172
V.4.3.1 Xây dựng cây cú pháp 172
V.4.3.2 Ví dụ : đạo hàm hình thức 173
CHƯƠNG VI MÔI TRƯỜNG VÀ CẤP PHÁT BỘ NHỚ 177
VI.1 Môi trường 177
VI.1.1 Một số khái niệm 177
VI.1.2 Phạm vi của một liên kết 178
VI.1.2.1 Phạm vi tĩnh 178
VI.1.2.2 Phép đóng = biểu thức lambda + môi trường 179
VI.1.2.3 Thay đổi bộ nhớ và phép đóng 180
VI.1.2.4 Nhận biết hàm 181
VI.1.2.5 Phạm vi động 182
VI.1.3 Thời gian sống của một liên kết 184
VI.1.4 Môi trường toàn cục 184
VI.2 Cấp phát bộ nhớ 185
VI.2.1 Ví dụ 1 : mô phỏng máy tính bỏ túi 186
VI.2.2 Ví dụ 2 : bài toán cân đối tài khoản 187
VI.3 Mô hình sử dụng môi trường 189
VI.4 Vào/ra dữ liệu 192
VI.4.1 Làm việc với các tệp 192
VI.4.2 Đọc dữ liệu trên tệp 193
1 Các hàm đọc tệp 193
2 Tệp văn bản 195
VI.4.3 Ghi lên tệp 196
1 Các hàm ghi lên tệp 196
2 Lệnh sao chép tệp 197
VI.4.4 Giao tiếp với hệ thống 198
PHỤ LỤC 203
TÀI LIỆU THAM KHẢO 205
Trang 97
LỜI NÓI ĐẦU
uốn sách này trình bày cơ sở lý thuyết và những kỹ thuật lập trình cơ bản theo phong cách «lập trình hàm» (Functional Programming) Đây là kết quả biên soạn từ các giáo trình sau nhiều năm giảng dạy bậc đại học và sau đại học ngành công nghệ thông tin của tác giả tại Đại học Đà Nẵng
Cuốn sách gồm sáu chương có nội dung như sau :
− Chương 1 giới thiệu quá trình phát triển và phân loại các ngôn ngữ lập trình, những đặc điểm cơ bản của phong cách lập trình mệnh lệnh Phần chính của chương trình bày nhữngnguyên lý lập trình hàm sử dụng ngôn ngữ minh hoạ Miranda
− Chương 2 trình bày những kiến thức mở đầu về ngôn ngữ Scheme : các khái niệm và các kiểu dữ liệu cơ sở, cách định nghĩa hàm, biểu thức, kỹ thuật sử dụng đệ quy và phép lặp
− Chương 3 trình bày các kiểu dữ liệu phức hợp của Scheme như chuỗi, vectơ, danh sách và cách vận dụng các kiểu dữ liệu trừu tượng trong định nghĩa hàm
− Chương 4 trình bày những kiến thức nâng cao về kỹ thuật lập trình hàm, định nghĩa hàm nhờ phép tính lambda, ứng dụng thuật toán quay lui, truyền thông điệp
− Chương 5 trình bày chi tiết hơn kỹ thuật lập trình nâng cao với Scheme sử dụng các cấu trúc dữ liệu : tập hợp, ngăn xếp, hàng đợi, cây và tệp
− Chương 6 trình bày khái niệm môi trường, cách tổ chức và cấp phát bộ nhớ, cách vào/ra dữ liệu của Scheme với thế giới bên ngoài
− Phần phụ lục giới thiệu vắn tắt ngôn ngữ lập trình WinScheme48, hướng dẫn cách cài đặt và sử dụng phần mềm này
Cuốn sách này làm tài liệu t khảo cho sinh viên các ngành công nghệ thông tin và những bạn đọc muốn tìm hiểu thêm về kỹ thuật lập trình cho lĩnh vực trí tuệ nhân tạo, giao tiếp hệ thống, xử lý ký hiệu, tính toán hình thức, các hệ thống đồ hoạ
Trong suốt quá trình biên soạn, tác giả đã nhận được từ các bạn đồng nghiệp nhiều đóng góp bổ ích về mặt chuyên môn, những động viên khích lệ về mặt tinh thần, sự giúp đỡ tận tình về biên tập để cuốn sách được ra đời Do mới xuất bản lần đầu tiên, tài liệu tham khảo chủ yếu là tiếng nước ngoài, chắc chắn rằng nội dung của cuốn sách vẫn còn bộc lộ nhiều thiếu sót, nhất là các thuật ngữ dịch ra tiếng Việt
Tác giả xin được bày tỏ ở đây lòng biết ơn sâu sắc về mọi ý kiến phê bình đóng góp của bạn đọc gần xa
Đà Nẵng, ngày 06/09/2004
Tác giả
C
Trang 10PHAN HUY KHÁNH
LẬP TRÌNH HÀM
Functional Programming
ập trình hàm là phong cách lập trình dựa trên định nghĩa hàm sử dụng phép tính lambda (λ-calculus) Lập trình hàm không sử dụng các lệnh gán biến và không gây ra hiệu ứng phụ như vẫn gặp trong lập trình mệnh lệnh Trong các ngôn ngữ lập trình hàm, hàm (thủ tục, chương trình con) đóng vai trò trung tâm, thay vì thực hiện lệnh, máy tính tính biểu thức Đã có rất nhiều ngôn ngữ hàm được phát triển và ứng dụng như Miranda, Haskell, ML, các ngôn ngữ
họ Lisp : Scheme, Common Lisp
Phần đầu cuốn sách này trình bày cơ sở lý thuyết và những khái niệm cơ bản của lập trình hàm sử dụng ngôn ngữ minh hoạ là Miranda, một ngôn ngữ thuần tuý hàm
do D Turner đề xuất 1986 Phần chính trình bày kỹ thuật lập trình hàm trong Scheme, một ngôn ngữ do Guy Lewis Steele Jr và G Jay Sussman đề xuất 1975
Ngôn ngữ Scheme có tính sư phạm cao, giải quyết thích hợp các bài toán toán học và
xử lý ký hiệu Scheme có cú pháp đơn giản, dễ học, dễ lập trình Một chương trình Scheme là một dãy các định nghĩa hàm góp lại để định nghĩa một hoặc nhiều hàm phức tạp hơn Scheme làm việc theo chế độ thông dịch, tương tác với người sử dụng
Cuốn sách này rất thích hợp cho sinh viên các ngành công nghệ thông tin và những bạn đọc muốn tìm hiểu về kỹ thuật lập trình ứng dụng trong lĩnh vực trí tuệ nhân tạo, giao tiếp người-hệ thống, xử lý ký hiệu, tính toán hình thức, thiết kế các
hệ thống đồ hoạ
VỀ TÁC GIẢ :
Tốt nghiệp ngành Toán Máy tính năm 1979 tại trường Đại học Bách khoa Hà Nội
Từ 1979 đến nay giảng dạy tại khoa Công nghệ Thông tin, trường Đại học Bách khoa,
Đạ i học Đà Nẵng Bảo vệ tiến sĩ năm 1991 tại Pháp Giữ chức chủ nhiệm khoa Công nghệ Thông tin 1995-2000
Hướng nghiên cứu chính : xử lý ngôn ngữ, xử lý đa ngữ, lý thuyết tính toán
E-mail: phanhuykhanh@dng.vnn.vn
L
Trang 11203
PHỤ LỤC
Scheme48 for Windows
Hiện nay có nhiều phiên bản trình thông dịch Scheme cung cấp miễn phí trên Internet Trong cuốn sách này, tác giả sử dụng chủ yếu Scheme48 for Windows, phiên bản 0.52, để chạy minh hoạ các ví dụ trong phần trình bày lý thuyết Đây là phần mềm được phát triển từ năm 1993 bởi R Kelsey và J Rees, sau đó được tiếp tục phát triển tại trường Đại học Northwestern, Chicago, Hoa Kỳ Phiên bản mới nhất hiện nay là WinScheme48 0.57 được tìm thấy tại trang web :
http://s48.org/index.html, hoặc trang web của trường Đại học Northwestern :
http://www.cs.nwu.edu/groups/su/edwin/
Bạn đọc có thể tìm thấy nhiều phiên bản Scheme khác như MITScheme, DrScheme, … Những phiên bản này không ngừng được cập nhật, đổi mới và có thư viện s-lib rất phong phú Tuy nhiên đối với những bạn đọc mới bắt đầu làm quen lập trình hàm với ngôn ngữ Scheme, chỉ nên chạy phiên bản 0.52 gọn nhẹ và tương đối ổn định trong mọi nền Win98/NT/2K hay XP được tải về từ địa chỉ sau :
http://www.cs.nwu.edu/groups/su/Scheme48/program/Scheme48.zip
Tiến trình cài đặt và thực hiện các thao tác chỉ định đường dẫn rất dễ dàng từ tệp nén
Scheme48.zip chứa đủ bộ thông dịch và tài liệu hướng dẫn Sau khi khởi động, màn hình làm
việc của Scheme48 for Windows như sau :
Hình 1 Phiên bản Scheme48 for Windows 0.52
WinScheme48 rất dễ thao tác và dễ sử dụng Cửa sổ làm việc chứa dấu nhắc lệnh là một dấu lớn hơn > Sau mỗi dòng lệnh, nếu s-biểu thức đưa vào đã đúng đắn về mặt cú pháp, WinScheme48 sẽ tiến hành thực hiện (evaluation) để trả về kết quả
Trang 12Để xem tài liệu hướng dẫn, NSD gọi chạy chương trình scheme48.chm có sẵn trong thư mục \Scheme48\doc đã được tự động tạo ra sau khi cài đặt (xem hình 2)
Hình 2 Hướng dẫn sử dụng Scheme48 for Windows 0.52
Trình soạn thảo văn bản thường trực của WinScheme48 tương tự NotePad/WordPad của Windows nên rất dễ sử dụng, nhờ các lệnh sao chép cut/paste Khi soạn thảo chương trình, các dòng lệnh được tự động thụt dòng (indentation) và thay đổi màu sắc giúp NSD dễ dàng kiểm tra cú pháp, phát hiện lỗi
Hình 3 Cửa sổ soạn thảo chương trình
Trong cửa sổ soạn thảo, NSD có thể định nghĩa các hàm, các biến, các chuỗi hay đưa vào các dòng chú thích WinScheme48 cung cấp bộ kiểm tra dấu ngoặc (parenthesis matching), chẳng hạn phần chương trình đứng trước dấu chèn có màu xanh dương cho biết s-biểu thức tương ứng đã hợp thức về mặt cú pháp, màu đỏ là xảy ra lỗi do thừa dấu ngoặc Sau khi soạn thảo các hàm, có thể thực hiện chương trình bằng cách:
• Lựa (highlight) vùng hàm (s-biểu thức) cần thực hiện, hoặc đặt con trỏ (dấu chèn) tại một vị trí bên trong
lệnh đơn (menu bar)
Trang 13205
Tài liệu tham khảo chính
[1] H Abdulrab de Common Lisp à la programmation objet
Editions HERMES, Paris 1990
[2] D Appleby & J.J.VandeKopple Programming Language: Paradigm and
Pratice THE MCGRAW−HILL COMPANIES,INC., 1997
[3] J Arsac Nhập môn lập trình Đinh Văn Phong và Trần Ngọc Trí dịch
Trung tâm Hệ thống Thông tin ISC, Hà Nội 1991
[4] H E Bal & D Grune Programming Language Essentials
ADDITION-WESLEY PUBLISHING COMPANY INC., 1994
[5] J Bentley Nhữngviên ngọc trong kỹ thuật lập trình Lê Minh Trung và Nguyễn
Văn Hiếu dịch, Trung tâm Tin học và Điện tử Phương Đông xuất bản, 1992
[6] J Chazarain Programmer avec Scheme – de la pratique à la théorie
INTERNATIONAL THOMSON PUBLISHING, Paris 1996
[7] W Clinger, J Rees & all Revised5 Prport on the Algorithmic Language
Scheme Tài liệu Internet : http://www.swiss.ai.mit.edu/~jaffer/r5rs_toc.html
[8] H Farrency & M Ghallab Elément d’intélligence artificielle
Editions HERMES, Paris 1990
[9] Ch Froidevaux và các tác giả khác Types de Données et Algorithmes
EDISCIENCE international, Paris 1994
[10] Phan Huy Khánh Giáo trình lập trình hàm
Giáo trình xuất bản nội bộ, Đại học Đà Nẵng 2002
[11] Phan Huy Khánh & Phan Chí Tùng Nhập môn Tin học
Nhà Xuất bản Giáo dục, 1997
[12] Đỗ Xuân Lôi Cấu trúc dữ liệu và giải thuật Nhà Xuất bản Giáo dục, 1993
[13] Ch Rapin Programmation Déclarative Springer Verlag 1993
[14] Y Rouzaud Algorithmique et Programmation en Scheme Tài liệu nội bộ,
ENSIMAG, Grenoble 1996
[15] D A Turner An Overview of Miranda Springer Lecture Notes in Computer
Science, vol 201, 1986
[16] Ngô Trung Việt Kiến thức cơ bản về lập trình
Nhà Xuất bản Giao thông Vận tải, 1995
[17] N Wirth "Algorithms + Data Structures = Programs", Prentice-Hall 1976
[18] J Malenfant Sémantique des langages de programmation
Tài liệu lấy từ internet : Université de Bretagne-Sud, Pháp
Trang 14i
Trang 161
« The primary purpose of a programming language
is to help the programmer in the practice of his art » Charle A Hoare (Hints on programming language design, 1973)
I.1 Mở đầu về ngôn ngữ lập trình
Buổi ban đầu
hững ngôn ngữ lập trình (programming language) đầu tiên trên máy tính điện tử là
ngôn ngữ máy (machine language), tổ hợp của các con số hệ hai, hay hệ nhị phân, hay
các bit (viết tắt của binary digit) 0 và 1 Ngôn ngữ máy phụ thuộc hoàn toàn vào kiến
trúc phần cứng của máy tính và những quy ước khắt khe của nhà chế tạo Để giải các bài toán, người lập trình phải sử dụng một tập hợp các lệnh điều khiển rất sơ cấp mà mỗi lệnh là một tổ hợp các số hệ hai nên gặp rất nhiều khó khăn, mệt nhọc, rất dễ mắc phải sai sót, nhưng lại rất khó sửa lỗi
Từ những năm 1950, để giảm nhẹ việc lập trình, người ta đưa vào kỹ thuật chương trình
con (sub-program hay sub-routine) và xây dựng các thư viện chương trình (library) để khi
cần thì gọi đến hoặc dùng lại những đoạn chương trình đã viết
Ngôn ngữ máy tiến gần đến ngôn ngữ tự nhiên
Cũng từ những năm 1950, ngôn ngữ hợp dịch, hay hợp ngữ (assembly) hay cũng còn
được gọi là ngôn ngữ biểu tượng (symbolic) ra đời Trong hợp ngữ, các mã lệnh và địa chỉ các toán hạng được thay thế bởi các từ tiếng Anh gợi nhớ (mnemonic) như ADD, SUB, MUL, DIV, JUMP tương ứng với các phép toán số học + - × /, phép chuyển điều khiển, v.v
Do máy tính chỉ hiểu ngôn ngữ máy, các chương trình viết bằng hợp ngữ không thể chạy ngay được mà phải qua giai đoạn hợp dịch (assembler) thành ngôn ngữ máy Tuy nhiên, các
hợp ngữ vẫn còn phụ thuộc vào phần cứng và xa lạ với ngôn ngữ tự nhiên (natural language), người lập trình vẫn còn gặp nhiều khó khăn khi giải các bài toán trên máy tính
Năm 1957, hãng IBM đưa ra ngôn ngữ FORTRAN (FORmula TRANslator) Đây là ngôn ngữ lập trình đầu tiên gần gũi ngôn ngữ tự nhiên với cách diễn đạt toán học FORTRAN cho phép giải quyết nhiều loại bài toán khoa học, kỹ thuật và sau đó được nhanh chóng ứng dụng rất rộng rãi cho đến ngày nay với kho tàng thư viện thuật toán rất đồ sộ và tiện dụng Tiếp theo là sự ra đời của các ngôn ngữ ALGOL 60 (ALGOrithmic Language) năm 1960, COBOL (Comon Business Oriented Language) năm 1964, Simula năm 1964, v.v
N
Trang 17Phát triển của ngôn ngữ lập trình
Theo sự phát triển của các thế hệ máy tính, các ngôn ngữ lập trình cũng không ngừng được cải tiến và hoàn thiện để càng ngày càng đáp ứng nhu cầu của người sử dụng và giảm
nhẹ công việc lập trình Rất nhiều ngôn ngữ lập trình đã ra đời trên nền tảng lý thuyết tính
toán (theory of computation) và hình thành hai loại ngôn ngữ : ngôn ngữ bậc thấp và ngôn ngữ bậc cao
Các ngôn ngữ bậc thấp (low-level language), hợp ngữ và ngôn ngữ máy, thường chỉ dùng
để viết các chương trình điều khiển và kiểm tra thiết bị, chương trình sửa lỗi (debugger) hay công cụ
Các ngôn ngữ lập trình bậc cao (high-level language) là phương tiện giúp người làm tin
học giải quyết các vấn đề thực tế nhưng đồng thời cũng là nơi mà những thành tựu nghiên cứu mới nhất của khoa học máy tính được đưa vào Lĩnh vực nghiên cứu phát triển các ngôn ngữ lập trình vừa có tính truyền thống, vừa có tính hiện đại Ngày nay, với những tiến bộ của khoa học công nghệ, người ta đã có thể sử dụng các công cụ hình thức cho phép giảm nhẹ công việc lập trình từ lúc phân tích, thiết kế cho đến sử dụng một ngôn ngữ lập trình
Các ngôn ngữ lập trình bậc cao được xây dựng mô phỏng ngôn ngữ tự nhiên, thường là tiếng Anh (hoặc tiếng Nga những năm trước đây) Định nghĩa một ngôn ngữ lập trình là định
nghĩa một văn phạm (grammar) để sinh ra các câu đúng của ngôn ngữ đó Có thể hình dung một văn phạm gồm bốn thành phần : bộ ký tự, bộ từ vựng, cú pháp và ngữ nghĩa
1 Bộ ký tự (character set)
Gồm một số hữu hạn các ký tự (hay ký hiệu) được phép dùng trong ngôn ngữ Trong các máy tính cá nhân, người ta thường sử dụng các ký tự ASCII Có thể hiểu bộ ký tự có vai trò như bảng chữ cái (alphabet) của một ngôn ngữ tự nhiên để tạo ra các từ (word)
2 Bộ từ vụng (vocabulary)
Gồm một tập hợp các từ, hay đơn vị từ vựng (token), được xây dựng từ bộ ký tự Các từ
dùng để tạo thành câu lệnh trong một chương trình và được phân loại tuỳ theo vai trò chức năng của chúng trong ngôn ngữ Chẳng hạn chương trình Pascal sau đây :
Trang 183 Cú pháp (syntax)
Cú pháp quy định cách thức kết hợp các ký tự thành từ, kết hợp các từ thành câu lệnh đúng (statement hay instruction), kết hợp các câu lệnh đúng thành một chương trình hoàn chỉnh về mặt văn phạm Có thể hình dung cách kết hợp này giống cách đặt câu trong một ngôn ngữ tự nhiên Thường người ta dùng sơ đồ cú pháp (syntax diagram) hoặc dạng chuẩn Backus-Naur (Backus-Naur Form, viết tắt BNF), hoặc dạng chuẩn Backus-Naur mở rộng (EBNF − Extended Backus-Naur Form) để mô tả cú pháp của văn phạm
Ví dụ I.1.1 : Trong ngôn ngữ Pascal (hoặc trong phần lớn các ngôn ngữ lập trình), tên gọi, hay định danh (identifier) có sơ đồ cú pháp như sau :
Hình I.1 Sơ đồ cú pháp tên trong ngôn ngữ Pascal
Trong một sơ đồ cú pháp, các ô hình chữ nhật lần lượt phải được thay thế bởi các ô hình tròn Quá trình thay thế thực hiện thứ tự theo chiều mũi tên cho đến khi nhận được câu đúng Chẳng hạn có thể «đọc» sơ đồ trên như sau : tên phải bắt đầu bằng chữ, tiếp theo có thể là chữ hoặc số tuỳ ý, chữ chỉ có thể là một trong các chũ cái A Za z, số chỉ có thể là một trong các chũ số 0 9 Như vậy, Delta, x1, x2, Read, v.v là các tên viết đúng, còn 1A, β, π, bán kính, v.v đều không phải là tên vì vi phạm quy tắc cú pháp
Văn phạm BNF gồm một dãy quy tắcc Mỗi quy tắc gồm vế trái, dấu định nghĩa ::= (đọc
được định nghĩa bởi ) và vế phải Vế trái là một ký hiệu phải được định nghĩa, còn vế phải là
một dãy các ký hiệu, hoặc được thừa nhận, hoặc đã được định nghĩa từ trước đó, tuân theo một quy ước nào đó EBNF dùng các ký tự quy ước như sau :
Ký hiệu Ý nghĩa
::=, hoặc →, hoặc = được định nghĩa là
Các quy tắc BNF định nghĩa tên trong ngôn ngữ Pascal :
<tên> ::= <chữ> { <chữ> | <số> }
<chữ> ::= ’A’ | | ’Z’ | ’a’ | | ’z’
<số> ::= ’0’ | | ’9’
Ví dụ I.1.2
Văn phạm của một ngôn ngữ lập trình đơn giản dạng EBNF như sau :
<statement> ::= <assignment> | <loop>
<assignment> ::= <identifier> := <expression> ;
tên
chữ
số chữ
Trang 19<loop> ::=
while <expression> do <statement>+ done
<expression> ::=
<value> | <value> + <value> | <value> <= <value>
<value> ::= <identifier> | <number>
<identifier> ::=
<letter>|<identifier><letter>|<identifier><digit>
<number> ::= <digit> | <number><digit>
Chương trình được viết trong một ngôn ngữ lập trình bậc cao, hoặc bằng hợp ngữ, đều được gọi là chương trình nguồn (source program)
Bản thân máy tính không hiểu được các câu lệnh trong một chương trình nguồn Chương
trình nguồn phải được dịch (translate) thành một chương trình đích (target program) trong ngôn ngữ máy (là các dãy số 0 và 1), máy mới có thể đọc «hiểu» và thực hiện được Chương trình đích còn được gọi là chương trình thực hiện (executable program)
Chương trình trung gian đảm nhiệm việc dịch đó được gọi là các chương trình dịch
Việc thiết kế chương trình dịch cho một ngôn ngữ lập trình đã cho là cực kỳ khó khăn và phức tạp Chương trình dịch về nguyên tắc phải viết trên ngôn ngữ máy để giải quyết vấn đề
xử lý ngôn ngữ và tính vạn năng của các chương trình nguồn Tuy nhiên, người ta thường sử dụng hợp ngữ để viết các chương trình dịch Bởi vì việc dịch một chương trình hợp ngữ ra ngôn ngữ máy đơn giản hơn nhiều Hiện nay, người ta cũng viết các chương trình dịch bằng chính các ngôn ngữ bậc cao hoặc các công cụ chuyên dụng
Thông thường có hai loại chương trình dịch, hay hai chế độ dịch, là trình biên dịch và trình thông dịch, hoạt động như sau :
• Trình biên dịch (compilater) dịch toàn bộ chương trình nguồn thành chương trình đích
rồi sau đó mới bắt đầu tiến hành thực hiện chương trình đích
• Trình thông dịch (interpreter) dịch lần lượt từng câu lệnh một của chương trình nguồn
rồi tiến hành thực hiện luôn câu lệnh đã dịch đó, cho tới khi thực hiện xong toàn bộ chương trình
Có thể hiểu trình biên dịch là dịch giả, trình thông dịch là thông dịch viên
Trang 20Những ngôn ngữ lập trình cấp cao ở chế độ biên dịch hay gặp là : Fortran, Cobol, C, C++, Pascal, Ada, Basic Ở chế độ thông dịch hay chế độ tương tác : Basic,Lisp, Prolog
Cho đến nay, đã có hàng trăm ngôn ngữ lập trình được đề xuất nhưng trên thực tế, chỉ có
một số ít ngôn ngữ được sử dụng rộng rãi Ngoài cách phân loại theo bậc như đã nói ở trên, người ta còn phân loại ngôn ngữ lập trình theo phương thức (paradigm), theo mức độ quan
trọng (measure of emphasis), theo thế hệ (generation), v.v
Cách phân loại theo bậc hay mức (level) là dựa trên mức độ trừu tượng so với các yếu tố phần cứng, chẳng hạn như lệnh (instructions) và cấp phát bộ nhớ (memory allocation)
Cao Biểu thức và điều khiển tường minh Truy cập và cấp phát nhờ các phép toán, chẳng hạn new FORTRAN, ALGOL, Pascal, C, AdaRất cao Máy trừu tượng Truy cập ẩn và tự động cấp phát SELT, Prolog, Miranda
Hình I.2 Ba mức của ngôn ngữ lập trình.
Những năm gần đây, ngôn ngữ lập trình được phát triển theo phương thức lập trình (còn được gọi
là phong cách hay kiểu lập trình) Một phương thức lập trình có thể được hiểu là một tập hợp các tính năng trừu tượng (abstract features) đặc trưng cho một lớp ngôn ngữ mà có nhiều người lập trình thường xuyên sử dụng chúng Sơ đồ sau đây minh hoạ sự phân cấp của các phương thức lập trình :
Hình I.3 Phân cấp của các phương thức lập trình
Sau đây là một số ngôn ngữ lập trình quen thuộc liệt kê theo phương thức :
• Các ngôn ngữ mệnh lệnh (imperative) có Fortran (1957), Cobol (1959), Basic (1965),
• Các ngôn ngữ dựa logic (logic-based) chủ yếu là ngôn ngữ Prolog (1970)
• Ngôn ngữ thao tác cơ sở dữ liệu như SQL (1980)
• Các ngôn ngữ xử lý song song (parallel) như Ada, Occam (1982), C-Linda,
Ngoài ra còn có một số phương thức lập trình đang được phát triển ứng dụng như :
Thủ tục đối tượng Hướng song song Xử lý Lôgic Hàm dữ liệu Cơ sở
Mệnh lệnh
Phương thức lập trình
Khai báo
Trang 21• Lập trình phân bổ (distributed programming)
• Lập trình ràng buộc (constraint programming)
• Lập trình hướng truy cập (access-oriented programming)
• Lập trình theo luồng dữ liệu (dataflow programming), v.v
Việc phân loại các ngôn ngữ lập trình theo mức độ quan trọng là dựa trên cái gì (what) sẽ thao tác được (achieved), hay tính được (computed), so với cách thao tác như thế nào (how)
Một ngôn ngữ thể hiện cái gì sẽ thao tác được mà không chỉ ra cách thao tác như thế nào được gọi là ngôn ngữ định nghĩa (definitional) hay khai báo (declarative) Một ngôn ngữ thể
hiện cách thao tác như thế nào mà không chỉ ra cái gì sẽ thao tác được gọi là ngôn ngữ thao
tác (operational) hay không khai báo (non-declarative), đó là các ngôn ngữ mệnh lệnh.
Hình I.4 Phát triển của ngôn ngữ lập trình
Các ngôn ngữ lập trình cũng được phân loại theo thế hệ như sau :
• Thế hệ 1 : ngôn ngữ máy
• Thế hệ 2 : hợp ngữ
• Thế hệ 3 : ngôn ngữ thủ tục
• Thế hệ 4 : ngôn ngữ áp dụng hay hàm
• Thế hệ 5 : ngôn ngữ suy diễn hay dựa logic
• Thế hệ 6 : mạng nơ-ron (neural networks)
Trang 22Trước khi nghiên cứu lớp các ngôn ngữ lập trình hàm, ta cần nhắc lại một số đặc điểm của lớp các ngôn ngữ lập trình mệnh lệnh
Trong các ngôn ngữ mệnh lệnh, người lập trình phải tìm cách diễn đạt được thuật toán,
cho biết làm cách nào để giải một bài toán đã cho Mô hình tính toán sử dụng một tập hợp
(hữu hạn) các trạng thái và sự thay đổi trạng thái Mỗi trạng thái phản ánh nội dung các biến
dữ liệu đã được khai báo Trạng thái luôn bị thay đổi do các lệnh điều khiển và các lệnh gán giá trị cho các biến trong chương trình Chương trình biên dịch cho phép lưu giữ các trạng thái trong bộ nhớ chính và thanh ghi, rồi chuyển các phép toán thay đổi trạng thái thành các lệnh máy để thực hiện
Hình I.5 Quan hệ giữa tên biến, kiểu và giá trị trong ngôn ngữ mệnh lệnh
Hình I.5 minh họa cách khai báo dữ liệu trong các ngôn ngữ mệnh lệnh và các mối quan
hệ theo mức Người ta phân biệt ba mức như sau : mức ngôn ngữ liên quan đến tên biến, tên kiểu dữ liệu và cấu trúc lưu trữ ; mức chương trình dịch liên quan đến phương pháp tổ chức
bộ nhớ và mức máy cho biết cách biểu diễn theo bit và giá trị dữ liệu tương ứng Mỗi khai
báo biến, ví dụ int i, nối kết (bind) tên biến (i) với một cấu trúc đặc trưng bởi tên kiểu
(int) và với một giá trị dữ liệu được biểu diễn theo bit nhờ lệnh gán i := 5 (hoặc nhờ một lệnh vừa khai báo vừa khởi gán int i=5) Tổ hợp tên, kiểu và giá trị đã tạo nên đặc trưng
của biến
Các ngôn ngữ mệnh lệnh được sử dụng hiệu quả trong lập trình do người lập trình có thể tác động trực tiếp vào phần cứng Tuy nhiên, tính thực dụng mệnh lệnh làm hạn chế trí tuệ của người lập trình do phải phụ thuộc vào cấu trúc vật lý của máy tính Người lập trình luôn
có khuynh hướng suy nghĩ về những vị trí lưu trữ dữ liệu đã được đặt tên (nguyên tắc địa chỉ hoá) mà nội dung của chúng thường xuyên bị thay đổi Thực tế có rất nhiều bài toán cần sự trừu tượng hoá khi giải quyết (nghĩa là không phụ thuộc vào cấu trúc vật lý của máy tính), không những đòi hỏi tính thành thạo của người lập trình, mà còn đòi hỏi kiến thức Toán học tốt và khả năng trừu tượng hoá của họ
Từ những lý do trên mà người ta tìm cách phát triển những mô hình tương tác không phản ánh mối quan hệ với phần cứng của máy tính, mà làm dễ dàng lập trình Ý tưởng của mô hình
là người lập trình cần đặc tả cái gì sẽ được tính toán mà không phải mô tả cách tính như thế
nào Sự khác nhau giữa «như thế nào» và «cái gì», cũng như sự khác nhau giữa các ngôn ngữ
Kiểu :
tập hợp giá trị tập hợp phép toán cấu trúc lưu trữ bit
Mức chương trình dịch
Mức máy
Trang 23mệnh lệnh và các ngôn ngữ khai báo, không phải luôn luôn rõ ràng Các ngôn ngữ khai báo thường khó cài đặt và khó vận hành hơn các ngôn ngữ mệnh lệnh Các ngôn ngữ mệnh lệnh thường gần gũi người lập trình hơn
Sau đây là một số đặc trưng của ngôn ngữ lập trình mệnh lệnh :
− Sử dụng nguyên lý tinh chế từng bước hay làm mịn dần, xử lý lần lượt các đối tượng
dữ liệu đã được đặt tên
− Khai báo dữ liệu để nối kết một tên biến đã được khai báo với một kiểu dữ liệu và một giá trị Phạm vi hoạt động (scope) của các biến trong chương trình được xác định bởi các khai báo, hoặc toàn cục (global), hoặc cục bộ (local)
− Các kiểu dữ liệu cơ bản thông dụng là số nguyên, số thực, ký tự và lôgic Các kiểu mới được xây dựng nhờ các kiểu cấu trúc Ví dụ kiểu mảng, kiểu bản ghi, kiểu tập hợp, kiểu liệt kê,
− Hai kiểu dữ liệu có cùng tên thì tương đương với nhau, hai cấu trúc dữ liệu là tương đương nếu có cùng giá trị và có cùng phép toán xử lý
− Trạng thái trong (bộ nhớ và thanh ghi) bị thay đổi bởi các lệnh gán Trạng thái ngoài (thiết bị ngoại vi) bị thay đổi bởi các lệnh vào-ra Giá trị được tính từ các biểu thức
− Các cấu trúc điều khiển là tuần tự, chọn lựa (rẽ nhánh), lặp và gọi chương trình con
− Chương trình con thường có hai dạng : dạng thủ tục (procedure) và dạng hàm
(function) Sự khác nhau chủ yếu là hàm luôn trả về một giá trị, còn thủ tục thì không không nhất thiết trả về giá trị Việc trao đổi tham biến (parameter passing) với chương
trình con hoặc theo trị (by value) và theo tham chiếu (by reference)
− Sử dụng chương trình con thường gây ra hiệu ứng phụ (side effect) do có thể làm thay
đổi biến toàn cục
− Một chương trình được xây dựng theo bốn mức : khối (block), chương trinh con, đơn
thể (module/packages) và chương trình
I.2 Cơ sở của các ngôn ngữ hàm
Trong các ngôn ngữ mệnh lệnh, một chương trình thường chứa ba lời gọi chương trình con (thủ tục, hàm) liên quan đến quá trình đưa vào dữ liệu, xử lý dữ liệu và đưa ra kết quả tính toán như sau :
Trang 24Các ngôn ngữ hàm là cũng các ngôn ngữ bậc cao, mang tính trừu tượng hơn so với các ngôn ngữ mệnh lệnh
Những người lập trình hàm thường tránh sử dụng các biến toàn cục, trong khi đó, hầu hết những người lập trình mệnh lệnh đều phải sử dụng đến biến toàn cục
Khi lập trình với các ngôn ngữ hàm, người lập trình phải định nghĩa các hàm toán học dễ suy luận, dễ hiểu mà không cần quan tâm chúng được cài đặt như thế nào trong máy
Những người theo khuynh hướng lập trình hàm cho rằng các lệnh trong một chương trình viết bằng ngôn ngữ mệnh lệnh làm thay đổi trạng thái toàn cục là hoàn toàn bất lợi Bởi vì rất nhiều phần khác nhau của chương trình (chẳng hạn các hàm, các thủ tục) tác động không trực tiếp lên các biến và do vậy làm chương trình khó hiểu Các thủ tục thường được gọi sử dụng
ở các phần khác nhau của chương trình gọi nên rất khó xác định các biến bị thay đổi như thế
nào sau lời gọi Như vậy, sự xuất hiện hiệu ứng phụ làm cản trở việc chứng minh tính đúng
đắn (correctness proof), cản trở tối ưu hóa (optimization), và cản trở quá trình song song tự động (automatic parrallelization) của chương trình
Một ngôn ngữ hàm, hay ngôn ngữ áp dụng (applicative language) dựa trên việc tính giá
trị của biểu thức được xây dựng từ bên ngoài lời gọi hàm Ở đây, hàm là một hàm toán học
thuần túy : là một ánh xạ nhận các giá trị lấy từ một miền xác định (domain) để trả về các giá
trị thuộc một miền khác (range hay co-domain)
Một hàm có thể có, hoặc không có, các tham đối (arguments hay parameters) để sau khi
tính toán, hàm trả về một giá trị nào đó Chẳng hạn có thể xem biểu thức 2 + 3 là hàm tính tổng (phép +) của hai tham đối là 2 và 3
Ta thấy rằng các hàm không gây ra hiệu ứng phụ trong trạng thái của chương trình, nếu trạng thái này được duy trì cho các tham đối của hàm Tính chất này đóng vai trò rất quan
trọng trong lập trình hàm Đó là kết quả của một hàm không phụ vào thời điểm (when) hàm
được gọi, mà chỉ phụ thuộc vào cách gọi nó như thế nào đối với các tham đối.
Trong ngôn ngữ lập trình mệnh lệnh, kết quả của biểu thức :
f(x) + f(x)
có thể khác với kết quả :
2 * f(x)
vì lời gọi f(x) đầu tiên có thể làm thay đổi x hoặc một biến nào đó được tiếp cận bởi f Trong
ngôn ngữ lập trình hàm, cả hai biểu thức trên luôn có cùng giá trị
Do các hàm không phụ thuộc nhiều vào các biến toàn cục, nên việc lập trình hàm sẽ dễ hiểu hơn lập trình mệnh lệnh Ví dụ giả sử một trình biên dịch cần tối ưu phép tính :
1
Ada là ngôn ngữ lập trình bậc cao được phát triển năm 1983 bởi Bộ Quốc phòng Mỹ (US Department of Defense), còn gọi là Ada 83, sau đó được phát triển bởi Barnes năm 1994, gọi là Ada 9X Ngôn ngữ Ada lấy tên của nhà nữ Toán học người Anh, Ada Augusta Lovelace, con gái của nhà thơ Lord Byron (1788−1824) Người
ta tôn vinh bà là người lập trình đầu tiên
Trang 25Một trình biên dịch song song sẽ gặp phải vấn đề tương tự nếu trình này muốn gọi hàm theo kiểu gọi song song
Bên cạnh tính ưu việt, ta cũng cần xem xét những bất lợi vốn có của lập trình hàm : nhược điểm của ngôn ngữ hàm là thiếu các lệnh gán và các biến toàn cục, sự khó khăn trong việc mô tả các cấu trúc dữ liệu và khó thực hiện quá trình vào/ra dữ liệu
Tuy nhiên, ta thấy rằng sự thiếu các lệnh gán và các biến toàn cục không ảnh hưởng hay không làm khó khăn nhiều cho việc lập trình Khi cần, lệnh gán giá trị cho các biến được mô phỏng bằng cách sử dụng cơ cấu tham biến của các hàm, ngay cả trong các chương trình viết bằng ngôn ngữ mệnh lệnh
Chẳng hạn ta xét một hàm P sử dụng một biến cục bộ x và trả về một giá trị có kiểu bất
kỳ nào đó (SomeType) Trong ngôn ngữ mệnh lệnh, hàm P có thể làm thay đổi x bởi gán cho x môt giá trị mới Trong một ngôn ngữ hàm, P có thể mô phỏng sự thay đổi này bởi truyền giá trị mới của x như là một tham đối cho một hàm phụ trợ thực hiện phần mã còn lại của P Chẳng hạn, sự thay đổi giá trị của biến trong chương trình P :
function P(n: integer) −> SomeType ;
ta có thể viết lại như sau :
function P(n : integer) −> SomeType ;
x: integer := n + 7
begin
return Q(3*x + 1) % mô phỏng x := x * 3 + 1
end ;
trong đó, hàm mới Q được định nghĩa như sau :
function Q(x: integer) −> Some Type
Một vấn để nổi bật trong ngôn ngữ hàm là sự thay đổi một cấu trúc dữ liệu Trong ngôn ngữ mệnh lệnh, sự thay đổi một phần tử của một mảng rất đơn giản Trong ngôn ngữ hàm, một mảng không thể bị thay đổi Người ta phải sao chép mảng, trừ ra phần tử sẽ bị thay đổi,
và thay thế giá trị mới cho phần tử này Cách tiếp cận này kém hiệu quả hơn so với phép gán cho phần tử
Một vấn đề khác của lập trình hàm là khả năng hạn chế trong giao tiếp giữa hệ thống tương tác với hệ điều hành hoặc với người sử dụng Tuy nhiên hiện nay, người ta có xu hướng tăng cường thư viện các hàm mẫu xử lý hướng đối tượng trên các giao diện đồ hoạ (GUI-Graphic User Interface) Chẳng hạn các phiên bản thông dịch họ Lisp như DrScheme, MITScheme, WinScheme
Trang 26Tóm lại, ngôn ngữ hàm dựa trên việc tính giá trị của biểu thức Các biến toàn cục và phép gán bị loại bỏ, giá trị được tính bởi một hàm chỉ phụ thuộc vào các tham đối Thông tin trạng thái được đưa ra tường minh, nhờ các tham đối của hàm và kết quả
Sau đây ta sẽ xét những khái niệm được coi là cơ bản nhất trong các ngôn ngữ hàm : hàm (function), danh sách (lists), kiểu (type), tính đa kiểu (polymorphism), các hàm bậc cao
(higher−order functions), tham đối hóa từng phần (Currying), tính hàm theo kiểu khôn
ngoan 2 (lazy evaluation), phương trình (equations), so khớp (patterm matching)
Các hàm đệ quy (recursive functions) là một trong những khái niệm chính trong ngôn ngữ hàm Các hàm bậc cao và phương pháp tính hàm theo kiểu khôn ngoan tạo thế mạnh cho lập trình hàm và đóng vai trò quan trọng trong việc xây dựng các chương trình hàm dạng đơn thể (modular functional programs) Tính đa kiểu bổ sung tính mềm dẻo cho hệ thống định kiểu Trước khi tìm hiểu ngôn ngữ Scheme bắt đầu từ chương 2, cuốn sách sử dụng ngôn ngữ Miranda để trình bày những khái niệm cơ bản của lập trình hàm Miranda là một ngôn ngữ hàm có cú pháp dễ đọc, dễ hiểu do David Turner phát triển năm 1986 Đặc điểm của Miranda
là thuần tuý hàm (purely functional), không xảy ra hiệu ứng phụ Một chương trình Miranda,
được gọi là một «script», là một tập hợp các phương trình (equation) được định nghĩa theo một thứ tự tuỳ ý nào đó (nói chung thứ tự không quan trọng)
Trình thông dịch Miranda chạy chế độ tương tác trong hệ điều hành Unix Sau đây, chúng tôi không trình bày đầy đủ cú pháp của Miranda mà chỉ qua các ví dụ của Miranda để minh
hoạ các yếu tố thuần tuý hàm của lập trình hàm
Ví dụ sau đây la một chương trình Miranda đơn giản Chú ý cặp ký hiệu || để bắt đầu một dòng chú thích của Miranda
Trong Miranda, một hàm được định nghĩa bởi hai phần : phần khai báo và phần định nghĩa hàm Phần khai báo có thể vắng mặt có dạng một nguyên mẫu hàm :
<tên hàm> :: <miền xác định> -> <miền giá trị>
Phần định nghĩa hàm có dạng một phương trình, gồm vế trái và một số vế phải, mỗi vế phải có thể có một điều kiện đóng vai trò «lính gác» (guard) phân biệt đứng cuối :
<tên hàm> [ <danh sách tham đối> ] = <biểu thức> [ <điều kiện> ]
Ví dụ hàm đổi nhiệt độ từ độ Fahrenheit (F) sang độ Celsius (C) được định nghĩa trong Miranda như sau :
celsius :: num −> num || khai báo kiểu hàm
2 Trong cuốn sách có nhiều thuật ngữ tiếng Việt do tác giả tự dịch từ tiếng Anh
Trang 27Dòng đầu tiên khai báo celsius là một hàm nhận một đối số và trả về một giá trị số Dòng thứ hai tính đổi nhiệt độ Áp dụng hàm celsius đã định nghĩa cho các đối số cụ thể,
Các hàm đệ quy đóng vai trò quan trọng trong các ngôn ngữ hàm Chẳng hạn, xét một ví
dụ cổ điển là tính ước số chung lớn nhất gcd (greatest common divisor) theo thuật toán
Euclide :
gcd a b = gcd (a-b) b, if a > b
= gcd a (b-a), if a < b
Hàm gcd được định nghĩa nhờ phương trình có 3 vế phải phân biệt, mỗi vế phải chứa
một điều kiện (a > b, a < b và a=b hay otherwise) Miranda sẽ thực hiện gọi đệ quy cho đến khi gặp điều kiện dừng là a=b và cũng là kết quả tính gcd Ta thấy các lính gác
trong phương trình thực hiện tương tự lệnh if trong ngôn ngữ mệnh lệnh
Trong các giáo trình nhập môn Tin học, người ta thường lấy ví dụ tính giai thừa của một
số nguyên không âm bằng phương pháp đệ quy Ta sẽ thấy ngôn ngữ Miranda tính giai thừa kiểu đệ quy như sau :
fac :: num −> num
Để so sánh, chương trình dưới đây chỉ ra cách tính n! trong một ngôn ngữ mệnh lệnh nhưng không dùng đệ quy (chỉ dùng đệ quy khi thật cần thiết !)
function fac (n: integer) : integer ;
var i, r: integer ; % r chứa kết quả
Sau đây là một ví dụ khác tính nghiệm một phương trình bậc hai ax 2 +bx+c=0 :
Trang 28quadsolve a b c
= [-b/(2*a)] + radix/(2*a),
where delta = b*b - 4*a*c and radix = sqrt delta
Mệnh đề của Miranda có thể lồng nhau (nested) nhiều mức
Trong hầu hết các ngôn ngữ hàm, danh sách (list) là cấu trúc dữ liệu quan trọng nhất
Trong ngôn ngữ Miranda, một danh sách gồm từ 0 đến nhiều phần tử có cùng kiểu, ví dụ :
nguyên week_days = ["Mon","Tue","Wed","Thur","Fri"]
Các danh sách có thể lồng nhau (bao hàm nhau) Một điểm mạnh của Miranda và của một
số ngôn ngữ hàm khác là cho phép định nghĩa một danh sách nhờ tính chất dễ nhận biết của
các phần tử của nó, tương tự cách xác định một tập hợp trong Toán học Một danh sách được
định nghĩa như vậy được gọi là nhận biết được hay ngầm hiểu (list comprehension) Ví dụ,
cách viết :
[ n | n <- [1 10] : n mod 2 = 1]
> [1, 3, 5, 7, 9]
chỉ định một danh sách có thứ tự của các số lẻ giữa 1 và 10 Danh sách được hiểu là «gồm tất
cả các số n nằm giữa 1 và 10 thoả mãn n mod 2 bằng 1» Một ví dụ khác về danh sách nhận
biết được :
[ n*n | n <- [1 100]] || danh sách bình phương các số giữa 1 và 100
Dạng tổng quát của danh sách nhận biết được :
[ body | qualifiers ]
trong đó mỗi qualifier hoặc là một bộ tạo sinh phần tử (generator) có dạng như sau :
var <- exp
ký hiệu <− biểu diễn phần tử var thuộc (∈) một tập hợp chỉ định bởi exp, hoặc là một bộ
lọc có dạng là một biểu thức lôgic thu hẹp miền xác định của các biến được tạo ra bởi bộ tạo
sinh Khi có nhiều thành phần qualifier cùng xuất hiện thì chúng được liệt kê cách nhau
một dấu : (dấu hai chấm)
Ví dụ sau đây trả về danh sách các hoán vị của một danh sách đã cho :
perms [] = [[]]
perms L = [ a:T | a <- L : T <- perms (L [a]) ]
Ta có thể định nghĩa lại hàm tính giai thừa nhờ hàm nhân product trong thư viện của
Miranda :
Trang 30length L = 0, if L = [] || danh sách rỗng có độ dài 0
|| danh sách khác rỗng có ít nhất một phần tử
length L = 1 + length (tl L), otherwise
Tương tự, hàm concatenate ghép hai danh sách được xây dựng như sau :
Dòng thứ hai là trường hợp tổng quát về ghép hai danh sách thành một danh sách mới sử dụng phép toán cons (otherwise) theo kiểu đệ quy Do danh sách thứ nhất L1 ≠ [] nên L1 có ít nhất một phần tử Khi đó danh sách mới (là danh sách kết quả) được tạo ra bởi phần
tử đầu tiên của L1, là (hd L1) ghép với (phép :) một danh sách còn lại Danh sách còn lại này lại là kết quả của bài toán ban đầu nhưng nhỏ hơn với tham đối thứ nhất là danh sách còn lại của L1 và tham đối thứ hai là toàn bộ L2 Quá trình cứ thế tiếp tục cho đến khi danh sách còn lại rỗng
Ta có thể so sánh cách xây dựng các danh sách trong các ngôn ngữ mệnh lệnh với ngôn ngữ Miranda
Trong các ngôn ngữ mệnh lệnh, danh sách do người lập trình tự xây dựng thường theo kiểu móc nối sử dụng biến con trỏ (pointer) và sử dụng bộ nhớ cấp phát động (dynamic allocation) Các phép toán trên danh sách là sự thao tác ở mức thấp trên các con trỏ Việc cấp phát và giải tỏa bộ nhớ cũng do người sử dụng tự giải quyết Thông thường, người lập trình
có xu hướng sử dụng việc cập nhật phá hủy (destructive updates) trên các cấu trúc dữ liệu
Ví dụ, người lập trình Ada ghép danh sách bằng cách viết một thủ tục cho phép móc nối (hook) hai danh sách với nhau nhờ một phép gán con trỏ, mà không cần xây dựng một danh sách mới (thứ ba) Tuy nhiên tiếp cận kiểu Ada có thể gây ra hiệu ứng phụ nếu làm thay đổi danh sách thứ nhất
Trong các ngôn ngữ hàm, người lập trình không hề quan tâm đến các con trỏ và việc cấp phát bộ nhớ Mặc dầu những yếu tố này là tồn tại trong ngôn ngữ hiện hành, nhưng chúng được che dấu một cách hiệu quả đối với người lập trình tạo nên một mức trừu tượng có lợi cho người lập trình, giúp họ tránh được những sai sót khi lập trình
Trong những tình huống như vậy, một hệ thống thời gian thực hiện RST (Run Time
System) quản lý bộ nhớ lưu trữ các phần tử của danh sách RTS cho phép cấp phát bộ nhớ khi cần thiết và tự động phát hiện những khối nhớ không còn cần dùng đến nữa để giải phóng chúng
Ngôn ngữ Miranda còn có kiểu dữ liệu tuple tương tự kiểu bản ghi của Pascal Đó là một dãy phần tử không có cùng kiểu (danh sách là một dãy phần tử có cùng kiểu tương tự kiểu mảng array) được viết giữa một cặp dấu ngoặc Ví dụ :
employee = ("Kim",True, False , 29)
Các phần tử kiểu tuple không có thứ tự Việc truy cập đến một phần tử chỉ có thể được thực hiện nhờ phép so khớp (pattern matching)
Trang 31I.2.4 Phép so khớp
Như đã thấy, một hàm trong Miranda có thể được định nghĩa bằng cách sử dụng nhiều biểu thức vế phải khác nhau, theo sau là «lính gác», đó là các điều kiện như if x>0, otherwise hay while Tuy nhiên, Miranda cho phép sử dụng một cú pháp tổng quát để lựa chọn giữa các khả năng dựa trên các mẫu (patterns) dạng :
<patterm> = <expression>, <condition>
trong đó, phần <condition> là tùy chọn Mẫu có thể sử dụng các tham biến hình thức, các
hằng và một số cấu trúc khác Khi một hàm được gọi, lời gọi hàm gồm tên hàm và các tham đối thực sự của nó (nếu có) sẽ được so khớp với các mẫu trong chương trình Nếu so khớp thành công, dòng lệnh tương ứng với mẫu được thực hiện Nếu có nhiều mẫu được tìm thấy, mẫu đầu tiên (theo thứ tự viết các lệnh) sẽ được chọn
Sau đây là một số ví dụ đơn giản minh họa cách định nghĩa các hàm sử dụng phép so khớp Hàm cond đã xét có thể viết dưới dạng :
Trường hợp tổng quát, một biểu thức số xuất hiện trong một mẫu có thể có dạng V + C, với V là một biến và C là một trực hằng (literal constant) Mẫu sẽ chỉ được so nếu V có thể
nhận một giá trị không âm Hàm ackerman được định nghĩa như sau :
fib (n+2) = fib (n+1) + fib n
Chú ý : Các biểu thức số tổng quát không thể được lấy làm các mẫu để tránh nhập nhằng (ambiguities) Ví dụ, mẫu :
Trang 32sách tham đối và do đó, L là danh sách còn lại trở thành tham đối mới của hàm Kết quả lời
gọi đệ quy đã giảm đi 1
Một số hàm xử lý danh sách khác sử dụng mẫu so khớp : tính tổng, tích và nghịch đảo một danh sách :
reverse (a:L) = reverse L ++ [a]
Để truy cập đến một phần tử của kiểu bản ghi (tuple), sử dụng so khớp, chẳng hạn bản ghi
có hai phần tử :
fst (a, b) = a || truy cập đến một phần tử thứ nhất
snd (a, b) = b || truy cập đến một phần tử thứ hai
Sau đây định nghĩa lại hai hàm thư viện của Miranda : hàm take trả về danh sách n phần tử đầu tiên của danh sách đã cho và hàm drop trả về phần còn lại của danh sách sau khi đã loại bỏ n phần tử đầu tiên
drop (n+1) (a:L) = drop n L
Chú ý rằng hai hàm được định nghĩa sao cho đồng nhất thức sau đây thoả mãn (bao gồm
cả trường hợp oái oăm là độ dài của L nhỏ thua n) :
take n L ++ drop n L = L
Một yếu tố quan trọng khác của hầu hết các ngôn ngữ hàm là phương pháp currying (lấy
tên nhà logic học Haskell B CURRY.), hay còn được gọi là phương pháp tham đối hoá từng
phần (partial parametrization)
Thông thường một hàm được thực hiện theo kết hợp trái, nếu viết f x y (hàm f tác động lên hai đối x y), viết quy ước (x, y) → f (x, y), thì cũng được xem như viết (f x) y, nghĩa là kết quả của việc áp dụng f cho x là một hàm để áp dụng cho y Ta viết x →(y → f (x, y))
Một cách tổng quát cho hàm n biến f (x 1 , x 2 , , x n) :
Trang 33Một cách tổng quát, một hàm nào đó có nhiều hơn một tham đối có thể được tham đối hóa từng phần Chẳng hạn ta định nghĩa hàm triple để nhân 3 một số :
cho kết quả là 21 vì dẫn đến lời gọi mult 3 7
Trong phần lớn các ngôn ngữ mệnh lệnh, các đối tượng cơ sở (biến và hằng) được xử lý khác với hàm Chúng có thể được đọc vào (từ bàn phím, từ tệp ), đưa ra (màn hình, máy in, tệp ), trao đổi giá trị với nhau, nhưng hàm chỉ có thể được gọi hoặc được kích hoạt (invoke)
Các đối tượng cơ sở được gọi là có bậc 0, các hàm có các tham đối là những đối tượng cơ
sở được gọi là các hàm bậc 1 Một hàm bậc n nhận các tham đối bậc nhỏ hơn n, để có các
biểu thức bậc cao hơn
Khái niệm về bậc được Russel (Russell's Paradox) và Whitehead (Principia Mathematica
~1900) đề xuất nhằm loại bỏ những nghịch lý (paradox) thường hay gặp trong lý thuyết tập hợp
của Cantor, chẳng hạn nghịch lý về người thợ cạo (barber paradox), bằng cách làm giảm tất cả các khả năng xảy ra vòng luẩn quẩn Trong lôgic vị từ bậc một, chỉ có thể sử dụng các lượng tử
áp dụng cho các cá thể (các biến) Còn trong lôgic vị từ bậc hai, người ta có thể lượng tử hoá các vị từ
Trong các ngôn ngữ hàm, hàm luôn luôn được xử lý như những đối tượng được truyền tham đối để trả về kết quả và được lưu giữ trong các cấu trúc dữ liệu Một hàm nhận một hàm khác như là một tham đối được gọi là hàm bậc cao Các hàm bậc cao mang lại tính hiệu quả
vowel = member ['a', 'e', 'i', 'o', 'u']
digit =
member ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9'] month =
member [ "Jan", "Feb", "Mar", "Apr", "Jun", "Jul", "Aug",
"Sep", "Oct", "Nov", "Dec" ] Các ngôn ngữ hàm đều có phép toán «bao qua phải» (foldr right) dùng để tính toán trên các phần tử của một danh sách Miranda có hàm foldr được định nghĩa như sau :
Trang 34foldr op k [] = k
foldr op k (a:L) = op a (foldr op k L)
Lời gọi hàm foldr :
and = foldr (&) true
gọi phép « và» lôgích cho tất cả các phần tử của một danh sách Kết quả phép gọi hàm and
L là true nếu mọi phần tử của danh sách L đều là true, kết quả là false nếu ngược lại Hàm nghịch đảo một danh sách :
reverse = foldr postfix []
where postfix a L = L ++ [a]
Các ngôn ngữ hàm có nhiều hàm bậc cao tiền định, và người lập trình cũng có thể tự định nghĩa những hàm bậc cao mạnh và tổng quát như hàm foldr vừa xét
Một hàm bậc cao quan trọng khác là map f L với f là một hàm và L là một danh sách Hàm map trả về kết quả là một danh sách mới gồm các phần tử của L đã được áp dụng cho hàm f Ví dụ :
Trang 35I.2.7 Kiểu và tính đa kiểu
Các ngôn ngữ hàm có một hệ thống kiểu dữ liệu hoàn toàn khác với các ngôn ngữ mệnh
lệnh Trong các ngôn ngữ mệnh lệnh (như Pascal, C, Ada ), tính định kiểu tĩnh chặt chẽ
(static strong typing) bắt buộc người lập trình phải mô tả kiểu cho mỗi biến dùng đến Sau khi khai báo, người sử dụng không được thay đổi kiểu dữ liệu của biến trong khi chạy chương trình
Các ngôn ngữ hàm thường không sử dụng định kiểu Tuy nhiên một số ngôn ngữ hàm lại
sử dụng định kiểu động (dymamic typing) hoặc định kiểu ẩn chặt chẽ (implicit strong typing)
(thường gặp trong ngôn ngữ Fortran)
Như vậy, các ngôn ngữ hàm tạo ra được tính mềm dẻo về các hàm đa kiểu (polymorphic
functions) với lời gọi có các tham đối có các kiểu dữ liệu khác nhau
Miranda sử dụng chế độ định kiểu tĩnh chặt chẽ tường minh (implicit static strong typing) Mọi biến mọi biểu thức đều có một kiểu được xác định tĩnh Có ba kiểu tiền định trong Miranda là num, bool và char Kiểu num không phân biệt số nguyên hay số thực Kiểu bool có hai giá trị hằng là true và false Kiểu char gồm các ký tự ASCII Một hằng ký tự được đặt giữa cặp dấu nháy đơn và sử dụng quy ước tương tự ngôn ngữ C, như
Nếu T1, , Tn là các kiểu nào đó, n>0, thì (T1, , Tn) là một kiểu bản ghi, hay bộ n
phần tử bất kỳ Ví dụ bộ 3 (true, ”Honda”, 7331) có kiểu (bool, [char], num) Nếu T1 và T2 là hai kiểu nào đó, thì T1->T2 là một kiểu hàm với tham đối có kiểu T1
và giá trị trả về có kiểu T2 Ví dụ hàm triple vừa định nghĩa trên đây có kiểu :
num −> num
Chú ý phép −> có kết hợp phải (right associative)
Người lập trình có thể sử dụng các khai báo định nghĩa hàm Ví dụ, hàm mult được khai báo kiểu currying như sau :
mult :: num −> (num −> num)
Có nghĩa rằng mult được xem là hàm có một tham đối trả về một hàm khác có kiểu : num −> num
Dấu :: trong khai báo hàm được đọc «có kiểu là » (is of type) Ví dụ :
sq :: num -> num
sq n = n * n
Tuy nhiên, việc khai báo kiểu hàm là không cần thiết Các ngôn ngữ hàm thường có khả năng uy diễn kiểu tự động nhờ một bộ kiểm tra kiểu (type checker) Khi không khai báo kiểu hàm, chẳng hạn nếu chỉ định nghĩa hàm triple bởi :
triple x = 3 * x
Trang 36thì bộ kiểm tra kiểu sẽ suy ra rằng x phải là một số, vì x là một thừa số trong một phép nhân
Bộ kiểm tra kiểu cũng suy ra được rằng kết quả của triple x phải là một số, do đó kiểu của hàm này phải là :
triple :: num −> num
Tính đa kiểu là yếu tố rất quan trọng trong nhiều ngôn ngữ hàm Với mỗi tham biến hình
thức (formal parameters), một hàm đa kiểu có thể chấp nhận lời gọi tương ứng với nhiều
tham đối thực sự (actual parameters) có các kiểu khác nhau Khác với các thủ tục trong Ada,
một hàm đa kiểu là một hàm đơn (single function), không phải là các hàm bội (multiple
functions) có cùng tên Ví dụ xét hàm đa kiểu pair sau đây trả về một danh sách từ hai tham đối là hai phần tử :
pháp sử dụng các biến kiểu đặc thù (generic type variables) Bên trong một khai báo kiểu,
một biến kiểu có thể chỉ định một kiểu nào đó, cùng một biến kiểu cho phép chỉ định cùng một kiểu xuyên suốt cả khai báo
Miranda sử dụng các ký hiệu *, **, *** để chỉ định kiểu tuỳ ý Ví dụ hàm đồng nhất id (identity) được định nghĩa như sau :
fac :: num -> num
ack :: num -> num-> num
sum :: [num] -> num
month :: [char] -> bool
Trang 37Các quy tắc định nghĩa kiểu như trên làm tránh được sai sót khi sử dụng hàm không đúng, chẳng hạn :
Trong ví dụ thứ hai, Miranda xem rằng kiểu của (pair 1 2) là một danh sách các số, nên không thể dùng để làm số hạng cho một phép cộng
Dù các hàm có thừa nhận các tham đối khác kiểu và không cần khai báo kiểu một cách tường minh, hệ thống kiểu vừa mô tả trên đây là chặt chẽ và ở dạng tĩnh Sự vi phạm các quy tắc định kiểu sẽ dẫn đến các thông báo lỗi khi dịch (compile time error messages)
Tuy nhiên việc thêm các khai báo kiểu hàm thường làm cho bộ kiểm tra kiểu dễ vi phạm sai sót khi sử dụng hàm
Thông thường, khi gọi tính giá trị một hàm, các tham đối được tính giá trị trước tiên, sau
đó mới tiến hành thực hiện tính toán trong hàm Chẳng hạn, lời gọi :
mult (fac 3) (fac 4)
yêu cầu tính giá trị các hàm giai thừa (fac 3) và (fac 4) trước, theo một thứ tự tùy ý, sau đó mới thực hiện hàm nhân :
mult 6 24
để cho kết quả cuối cùng là 144 Cách viết rút gọn biểu thức lúc đầu thành dạng đơn giản hơn được gọi là phép rút gọn theo thứ tự áp dụng (applicative order reduction) Chú ý rằng phép rút gọn được bắt đầu từ các biểu thức trong cùng nhất (innermost expressions)
Tuy nhiên có một cách khác để rút gọn một biểu thức là bắt đầu từ biểu thức ngoài nhất (outermost expression) và không tính giá trị các biểu thức con (subexpressions) cho đến khi cần dùng đến kết quả của chúng
Người ta gọi cách này là rút gọn theo thứ tự thường (normal order reduction) Chẳng hạn
Giả sử rằng ta muốn xây dựng hàm cond nhận một giá trị bool b như là tham đối thứ nhất và trả về tham đối thứ hai của nó nếu b có giá trị true hoặc trả về tham đối thứ ba nếu
b là false :
cond b x y = x, if b
cond b x y = y, otherwise
hay gọn hơn :
Trang 38cond true x y = x
cond false x y = y
Nếu cả ba tham đối của cond được tính giá trị trước khi hàm cond được thực hiện, thì
sẽ xảy ra hai trường hợp :
− Một trong các tham đối của cond tham gia tính toán một cách vô ích, dẫn đến kết quả sai, chẳng hạn cond (x=0) 0 (1/x) với x=0
− Nếu việc tính toán biểu thức tham đối là vô ích và không kết thúc thì sẽ gây ra toàn bộ biểu thức bị tính lặp vô hạn lần
Trường hợp thứ hai có thể được minh họa qua ví dụ sau : giả sử ta cũng sử dụng hàm cond cho một định nghĩa khác của hàm fac :
fac n = cond (n = 0) 1 (n * fac (n - 1))
Nếu tham đối thứ ba của cond luôn luôn được tính, thì hàm fac sẽ không bao giờ dừng Bởi vì lời gọi fac 1 kéo theo lời gọi fac 0, lời gọi fac 0 sẽ kéo theo lời gọi fac − 1
và cứ thế tiếp tục
Những vấn đề trên đã dẫn đến khái niệm tính giá trị theo kiểu khôn ngoan trong lập trình hàm sử dụng phép rút gọn theo thứ tự thường Ý tưởng của phương pháp tính giá trị hàm theo kiểu khôn ngoan là chỉ tính giá trị các tham đối của một hàm khi các giá trị của chúng là cần thiết, những biểu thức nào không cần thiết thì bỏ qua
Sử dụng tính giá trị hàm theo kiểu khôn ngoan, phép tính hàm fac 1 sẽ được rút gọn như được trình bày dưới đây Việc tính giá trị kết thúc và cho kết quả đúng Ơ đây ta sử dụng biểu thức if dạng giả ngữ (pseuclo-notation) Tham đối thứ ba của cond sẽ không được tính nếu n bằng 0, vì rằng trong trường hợp này, không cần tính tham đối nữa
fac 1
> if (1=0) then 1 else (1*fac (1-1)) || gọi cond
> 1*fac (1-1)
> 1*((if 1-1=0) then 1 else ((1-1)*fac ((1-1)-1)))
|| gọi cond > 1*((if true then 1 else ((1-1)*fac ((1-1)-1)))
Trang 39> 2 || phần tử thứ hai
hd (tl (map triple [1 ]))
Thực tế, ngôn ngữ Miranda chỉ cho phép xây dựng các danh sách hữu hạn Chỉ khi toàn
bộ các phần tử của danh sách được yêu cầu, như là :
hoặc :
sẽ làm cho hệ thống rơi vào một vòng lặp vô hạn (hoặc gây tràn bộ nhớ) Sau đây là một số ví
where sieve (p:x)=p : sieve [n|n<-x: n mod p>0 ]
Mặc dù phương pháp tính giá trị hàm theo kiểu khôn ngoan tỏ ra có nhiều ưu thế nhưng thực tế, việc cài đặt rất tốn kém Một vấn đề xảy ra là chiến lược rút gọn theo thứ tự thường
có thể tính một biểu thức mất nhiều lần, trong khi phép rút gọn theo thứ tự áp dụng chỉ tính giá trị các biểu thức đúng một lần Chẳng hạn, từ định nghĩa hàm double sau đây :
Một vấn đề khác là tính giá trị của các tham đối trước khi gọi hàm Thực tế cho thấy việc tính trước tham đối dễ dàng cài đặt hơn là trì hoãn thực hiện chúng Tuy nhiên, điều này làm
Trang 40tổng chi phí tính giá trị hàm theo kiểu khôn ngoan cao hơn bình thường Hiện nay, người ta đang phát triển các phương pháp tối ưu nhằm giảm tổng chi phí này
Không phải mọi ngôn ngữ hàm đều có kiểu tính khôn ngoan như vừa trình bày Những ngôn ngữ không có khả năng này cho các hàm do người sử dụng tự xây dựng (user defined functions) thường có sẵn một vài hàm với ngữ nghĩa khôn ngoan (ví dụ hàm cond) Các ngôn ngữ mệnh lệnh cũng có những yếu tố mang ngữ nghĩa khôn ngoan được xây dựng sẵn, chẳng hạn lệnh if Ngôn ngữ C và Ada có các phép logic (dạng short cut) and và or đặc biệt cho phép tính giá trị tham đối thứ hai chỉ khi cần thiết
Sau đây ta xét một số ví dụ khác viết trong Miranda để minh họa những đặc trưng cơ bản của lập trình hàm vừa được trình bày trên đây : loại bỏ những phần tử trùng nhau trong một danh sách đã được sắp xếp thứ tự, sắp xếp nhanh một danh sách (quicksort), giải bài toán 8 quân hậu và tìm dãy số Hamming
Ta cần xây dựng hàm uniq nhận một danh sách các phần tử đã được sắp xếp để trả về một danh sách đã loại bỏ những phần tử trùng nhau đứng trước, chỉ giữ lại một phần tử đứng sau cùng Ta có chương trình như sau :
uniq (a:(a:L)) = uniq (a:L) || bỏ một trong hai phần tử đầu tiên bằng nhau
Dòng chương trình thứ nhất tầm thường : danh sách rỗng xem như đã được xử lý Dòng thứ hai sử dụng khả năng thể hiện một biến (a) hai lần trong cùng một vế trái Một khi dòng này được tiếp cận, thì một lời gọi đệ quy sẽ xuất hiện với tham đối là danh sách ban đầu nhưng đã loại bỏ phần tử đầu (head) do trùng nhau Chẳng hạn danh sách [3, 3, 4] chỉ còn lại [3, 4] phải xử lý Nếu như cả hai dòng chương trình đầu bị bỏ qua thì dòng thứ ba được chọn Dòng này này cũng tạo ra một lời gọi đệ quy với phần tử đầu tiên của danh sách
là cái ra của nó Chẳng hạn danh sách [3, 4, 5] dẫn đến chỉ còn phải xử lý [4, 5] Cách định nghĩa hàm uniq trên đây sử dụng các mẫu so khớp Một cách khác là sử dụng các điều kiện «lính canh» ở vế phải nhưng như vậy sẽ làm chương trình trở nên dài dòng hơn
Cuối cùng, ta thấy hàm uniq là đa kiểu Hàm có thể được gọi với nhiều kiểu khác nhau của tham đối thực sự Chẳng hạn :