Haskell là một ngôn ngữ lập trình hàm thuần túy và mang tính trì hoãn. Sở dĩ Haskell có tính chất trì hoãn bởi lẽ khi đi tìm câu trả lời cho 1 vấn đề, những tham số không cần thiết sẽ không được định trị. Đối nghịch với tính chất trì hoãn là tính chất tức thì – chiến lược định trị của đa phần các ngôn ngữ lập trình hiện nay (C, C++, Java, thậm chí là ML – một ngôn ngữ lập trình hàm). Một ngôn ngữ có tinh chất tức thì có nghĩa là mọi biểu thức đều được định trị cho dù kết quả đó có được sử dụng hay không. Haskell được gọi là ngôn ngữ lập trình hàm thuần túy bởi lẽ nó không cho phép các hiệu ứng phụ điều có thể làm thay đổi trạng thái của hệ thống. Việc một ngôn ngữ lập trình không có hiệu ứng phụ sẽ không quá kinh khủng; Haskell sử dụng một hệ thống đơn nguyên để tách biệt những tính toán không thuần túy ra khỏi phần còn lại của chương trình để thực hiện chúng một cách an toàn. Chúng ta sẽ nghiên cứu rõ hơn vấn đề này ở các phần tiếp theo về đơn nguyên và vàora trong ngôn ngữ thuần túy. Haskell được gọi là ngôn ngữ lập trình hàm bởi vì việc định trị một chương trình tương đương với định trị một hàm trên khía cạnh toán học thuần túy. Điều này khác biệt khi so sánh với các ngôn ngữ chuẩn (Ví dụ như C và Java) với cách định trị một chuỗi các statements.
Trang 11 Tổng quan về Haskell 3
1.1 Giới thiệu 3
1.2 Lịch sử của Haskell 3
Vì sao chúng ta cần đến Haskell? 5
Điểm yếu của Haskell? 5
1.3 Cấu trúc chương trình 5
2 Cấu trúc từ vựng và các khái niệm tổng quát trong Haskell 6
2.1 Quy ước ký hiệu 6
2.2 Cấu trúc từ vựng chương trình 7
2.3 Chú thích 8
2.4 Định danh và các toán tử 9
2.5 Chữ kiểu số 10
2.6 Kiểu ký tự và kiểu chuỗi 11
2.7 Khai báo và liên kết trong Haskell 12
2.8 Tổng quan về các kiểu và các lớp 13
2.8.1 Các loại 14
2.8.2 Cú pháp của kiểu 15
2.8.3 Cú pháp xác nhận lớp và ngữ cảnh 16
2.8.4 Ngữ nghĩa của kiểu và lớp 17
3 Giá trị, kiểu và các khái niệm 17
3.1 Kiểu đa hình 19
3.2 Các kiểu do người dùng tự định nghĩa 21
3.3 Kiểu đồng nghĩa 23
3.4 Các kiểu được xây dựng sẵn 24
3.4.1 Thể hiện danh sách và chuỗi số học 26
3.4.2 Chuỗi 26
4 Hàm trong Haskell 27
4.1 Trừu tượng hóa Lambda 28
4.2 Toán tử trung tố 28
4.2.1 Section 29
4.2.2 Khai báo cố định (fixity declarations) 29
4.3 Tính không chặt của hàm 30
4.4 Dữ liệu không giới hạn (Infinite data structure) 31
4.5 Hàm lỗi (error function) 32
5 Biểu thức theo trường hợp và khớp mẫu 33
5.1 Ngữ nghĩa của khớp mẫu 34
5.2 Ví dụ 35
5.3 Biểu thức trường hợp 36
5.4 Mẫu Lazy 37
Trang 25.5 Phạm vi từ vựng và các dạng lồng nhau 40
5.6.Trình bày 41
6 Các lớp kiểu và viết chồng 42
7 Các thao tác vào ra cơ bản trong Haskell 49
8 Những ưu điểm của Haskell 50
8.1 Haskell là một ngôn ngữ lập trình hàm thuần túy 50
8.2 Haskell được đánh giá là 1 trong những ngôn ngữ có syntax đẹp nhất 50
8.3 Tính biểu cảm cao 51
8.4 Tính sử dụng lại 52
8.5 Định trị trì hoãn 52
8.6 Khả năng trừu tượng hóa cao 53
9 Kết luận 53
Haskell đang trở thành xu thế 53
TÀI LIỆU THAM KHẢO 55
Trang 3sử dụng hay không
Haskell được gọi là ngôn ngữ lập trình hàm thuần túy bởi lẽ nó không cho phépcác hiệu ứng phụ - điều có thể làm thay đổi trạng thái của hệ thống Việc một ngônngữ lập trình không có hiệu ứng phụ sẽ không quá kinh khủng; Haskell sử dụngmột hệ thống đơn nguyên để tách biệt những tính toán không thuần túy ra khỏiphần còn lại của chương trình để thực hiện chúng một cách an toàn Chúng ta sẽnghiên cứu rõ hơn vấn đề này ở các phần tiếp theo về đơn nguyên và vào/ra trongngôn ngữ thuần túy
Haskell được gọi là ngôn ngữ lập trình hàm bởi vì việc định trị một chương trìnhtương đương với định trị một hàm trên khía cạnh toán học thuần túy Điều nàykhác biệt khi so sánh với các ngôn ngữ chuẩn (Ví dụ như C và Java) với cách địnhtrị một chuỗi các statements
1.2 Lịch sử của Haskell
Lịch sử của Haskell được trích từ báo cáo Haskell 98 của chính các tác giả nhưsau Tháng 9 năm 1987, tại hội thảo về Các ngôn ngữ lập trình hàm và Kiến trúcmáy tính (FPCA ‘87) tại Portland, Oregon để bàn luận về một vấn đề trong cộngđồng những người nghiên cứu về lập trình hàm: Có hơn một tá các ngôn ngữ lậptrình hàm thuần túy và tất cả chúng đều giống nhau về khả năng biểu diễn và nền
Trang 4tảng ngữ nghĩa Hội thảo đã đạt được sự nhất trí cao về việc thiếu một ngôn ngữphổ dụng đã cản trở sử dụng rộng rãi lớp ngôn ngữ lập trình hàm này Hội nghị đãquyết định thành lập một hội đồng để tạo ra ngôn ngữ đó, đồng thời nhanh chóngkết nối các ý tưởng mới, là nền tảng vững chắc để phát triển các ứng dụng thực tế,
là phương tiện để khuyến khích mọi người sử dụng ngôn ngữ lập trình hàm Tàiliệu này mô tả những nỗ lực của hội đồng, đó là một ngôn ngữ lập trình hàm thuầntúy gọi là Haskell Tên gọi này được đặt theo tên của nhà logic học HaskellB.Curry – người có những công trình là nền tảng logic cho ngôn ngữ
Thành công chính của hội nghị là sáng tạo ra một ngôn ngữ thỏa mãn nhữngràng buộc về:
Thích hợp để giảng dạy, nghiên cứu, xây dựng các ứng dụng, bao gồm cảviệc xây dựng các hệ thống lớn
Có thể được mô tả hoàn toàn qua các công bố về cú pháp và ngữ nghĩa
Miễn phí: mọi người đều được phép cài đặt, xây dựng các hệ thống trên nềntảng ngôn ngữ
Được xây dựng trên những ý tưởng có sự nhất trí cao
Làm giảm sự tràn lan của các ngôn ngữ lập trình hàm
Hội đồng dự kiến rằng Haskell sẽ trở thành nền tảng cho những nghiên cứu vềthiết kế ngôn ngữ và hi vọng về sự mở rộng của thế giới ngôn ngữ cùng với cáckết quả thử nghiệm
Quả thật, Haskell đã không ngừng mở rộng kể từ những công bố đầu tiên Giữanăm 1997 đã có 4 phiên bản thiết kế của ngôn ngữ này (bản mới nhất vào lúc đóalf Haskell 1.4) Năm 1997, hội thảo Haskell tại Hà Lan, các nhà nghiên cứu đã
quyết định về phiên bản ổn định của Haskell – Đó là Haskell 98.
Haskell 98 được xem như là bản rút gọn của Haskell 1.4, được đơn giản hóa vàloại bỏ một số lỗi có thể mắc phải Haskell được dự kiến sẽ là ngôn ngữ mang tính
ổn định theo nghĩa trong tương lai, bộ thực hiện cam kết sẽ hỗ trợ cho Haskell 98chính xác như đã chỉ ra
Trang 5Vì sao chúng ta cần đến Haskell?
Có rất nhiều lý do để sử dụng Haskell Haskell là một trong những ngôn ngữ chophép tạo nên những đoạn mã lệnh trong thời gian ngắn nhất với ít lỗi nhất Đồngthời, chương trình viết bằng Haskell rất dễ đọc với khả năng mở rộng cao Ví dụ:
factorial 1 = 1
factorial n = n * factorial (n-1)
Ví dụ trên thực hiện việc tính giai thừa, với cách viết gần gũi với tư duy đệ quy.Tuy nhiên, có lẽ điều quan trọng nhất là những người sử dụng Haskell tạo nênmột cộng đồng với nhiều chia sẻ rất hữu ích Hiện tại, Haskell vẫn luôn thay đổi
và những phản hồi của người sử dụng luôn được lưu ý để tạo nên thay đổi trongcác phiên bản mới
Điểm yếu của Haskell?
Dưới đây là hai trong số những vấn đề những người sử dụng Haskell than phiền:
Mã lệnh được sinh ra có vẻ chậm hơn các chương trình viết trên các ngônngữ khác như C
Có vẻ khó debug hơn
Vấn đề thứ hai không phải là vấn đề quá lớn bởi lẽ Haskell giúp tạo ra nhữngđoạn mã với ít lỗi Vấn đề thứ nhất cũng khá phổ biến, tuy nhiên, thời gian tínhtoán là rẻ hơn so với thời gian lập trình nên nếu phải đợi thêm 1 chút khi thực thi
để đổi đấy vài ngày debug thì cũng xứng đáng Tuy vậy, vấn đề này xảy ra khôngphải với mọi ứng dụng Haskell có một thư viện các giao diện cho phép ghép nốivới những đoạn mã lệnh viết trên các ngôn ngữ khác khi cần tối ưu hóa thời gianthực thi
1.3 Cấu trúc chương trình
1 Ở mức cao nhất của 1 chương trình Haskell là tập hợp các modules Cácmodules này cung cấp phương thức điều khiển không gian tên phục vụ cho mụcđích sử dụng lại trong các chương trình lớn
Trang 62 Mức trên cùng của một module chứa một tập rất nhiều các kiểu khai báo Cáckhai báo này định nghĩa những thành phần sẽ được dùng sau đó ví dụ như các giátrị nguyên thủy, kiểu dữ liệu, các lớp kiểu và các thông tin cố định.
3 Mức thấp hơn là các biểu thức – phần quan trọng nhất của lập trình Haskell.Các biểu thức chỉ ra một giá trị và có một kiểu tĩnh
4 Ở mức dưới cùng của Haskell là cấu trúc từ vựng Cấu trúc từ vựng biểu diễn
cụ thể các chương trình Haskell dưới dạng text
Ta sẽ nghiên cứu Haskell từ dưới lên
2 Cấu trúc từ vựng và các khái niệm tổng quát trong Haskell
2.1 Quy ước ký hiệu
Các quy ước ký hiệu để biểu diễn cú pháp của Haskell như sau:
pat<pat’> hiệu – những thành phần sinh ra bởi pat mà không được sinh ra bởi pat’
Cú pháp dạng tương tự BNF được sử dụng với các luật sản xuất có dạng sau:
Trang 72.2 Cấu trúc từ vựng chương trình
Trang 8
Kỹ thuật phân tích từ vựng sử dụng luật “nhai cực đại”: tại mỗi điểm, vị từ dàinhất thỏa mãn luật sản xuất vị từ được đọc vào Vì vậy, case là một từ được lưu lạitrong khi cases thì không Tương tự như vậy, = được lưu nhưng == và ~= thìkhông
Mọi kiểu của các khoảng trống đều là ranh giới cho các vị từ Các ký tự khôngnằm trong kiểu ANY thì không phù hợp trong Haskell và sẽ gây ra lỗi
2.3 Chú thích
Một chú thích thông thường sẽ bắt đầu bằng hai hay nhiều các gạch nối (ví dụ: )
và mở rộng sang cả dòng mới Chuỗi tuần tự các gạch nối này không được phéptạo thành một vị từ có nghĩa nào
Ví dụ: “ >” hay “| “ không phải là bắt đầu của một chú thích bởi vì chúng đều
là các vị từ có nghĩa Tuy vậy, “ foo” bắt đầu cho một chú thích
Các chú thích lồng nhau được bắt đầu bởi “{-” và kết thúc bởi “-}” Không có vị
từ có nghĩa nào bắt đầu bằng “{-” nên “{ -” sẽ bắt đầu cá chú thích lồng nhaumặc dù các gạch nối được đặt lien tiếp
Bản thân các chú thích sẽ không được phân tích về mặt từ vựng, kí hiệu “-}” sẽkết thúc chú thích lồng nhau Các chú thích lồng nhau này có thể có độ sâu tùy ý,mọi ký hiệu “{-“ sẽ bắt đầu cho một chú thích và sẽ được kết thúc bởi “-}” Trongcác chú thích lồng nhau thì mỗi ký hiệu “{-” sẽ tương ứng với một kí hiệu “-}”.Các chú thích lồng nhau này cũng được sử dụng cho trình biên dịch pragmas vớinhững chỉ thị cho trình biên dịch làm việc
Trang 92.4 Định danh và các toán tử
Một định danh bao gồm một chữ cái và kế đó là không hoặc nhiều các chữ cái,
số, dấu gạch dưới và dấu nháy đơn khác nữa Các định danh được phân biệt vềmặt từ vựng thành 2 miền không gian tên: loại bắt đầu bằng chữ cái thường (địnhdanh kiểu biến) và loại bắt đầu bằng chữ cái viết hoa (định danh kiểu khởi tạo).Các định danh trong Haskell có phân biệt chữ hoa và chữ thường: các định danhname, naMe và Name là khác nhau (2 định danh đầu tiên là định danh kiểu biến,định danh cuối cùng là định danh kiểu khởi tạo)
Dấu gạch dưới “_” được xem như 1 ký tự chữ thường và có thể xuất hiện ở mọi
vị trí của chữ thường Tuy vậy, một số trình biên dịch đưa ra những chú ý chonhững định danh bắt đầu bởi “_” Điều này cho phép các lập trình viên sử dụng
“_foo” như một tham số mà họ sẽ không sử dụng
Các ký hiệu toán tử được tạo thành từ một hay nhiều các kí tự ký hiệu Như đãchỉ ra ở trên, chúng được phân biệt về mặt từ vựng thành 2 miền không gian tên:
Một kí hiệu toán tử bắt đầu bởi 1 dấu “:” là một toán tử khởi tạo
Một kí hiệu toán tử bắt đầu bởi bất kỳ ký hiệu khác là một định danh thôngthường
Trang 10Chú ý rằng bản thân dấu hai chấm được dành để khởi tạo danh sách trongHaskell, điều này cũng giống như khi làm việc với các phần khác trong cú phápcủa danh sách, ví dụ như “[]” hay “[a, b]”.
Trong phần tiếp theo của báo cáo, 6 loại tên khác nhau sẽ được sử dụng:
2.5 Chữ kiểu số
Có hai loại kiểu số khác nhau: đó là integer và float Kiểu số nguyên thường đượccho dưới dạng số thập phân, hệ cơ số 8 (với tiền tố 0o hoặc 0O), hoặc hệ cơ số 16(với tiền tố 0x hay 0X) Kiểu số thực luôn có dạng số thập phân và luôn có các chữ
số phía trước và sau dấu chấm (điều này giúp không gây nhầm lẫn khi sử dụng dấuchấm)
Trang 112.6 Kiểu ký tự và kiểu chuỗi
Trong Haskell, kiểu kí tự được viết trong dấu nháy đơn, ví dụ như ‘a’ và kiểuchuỗi được viết trong dấu nháy kép, ví dụ như “Hello” Chú ý, cần phải tránh dùngdấu nháy đơn với kiểu kí tự, nhưng với kiểu chuỗi thì được Tương tự như vậy,dấu nháy kép có thể dùng được với ký tự nhưng với kiểu chuỗi thì nên tránh \
luôn là bắt đầu của kí tự điều khiển Loại charesc bao gồm những biểu diễn cho
các kí tự “alert” (\a), “backspace” (\b), “form feed” (\f), “new line” (\n), “carriagereturn” (\r), “horizontal tab” (\t), and “vertical tab” (\v)
Một chuỗi có thể chứa một khoảng cách (hai dấu gạch chéo ngược bọc lấy cáckhoảng trắng) Điều này cho phép ta viết các chuỗi dài trên nhiều dòng với mộtdấu gạch chéo ở cuối dòng trên và một dấu gạch chéo ở đầu dòng dưới
Trang 122.7 Khai báo và liên kết trong Haskell
Phần này sẽ trình bày cú pháp và ngữ nghĩa của các khai báo trong Haskell
Các khai báo trong phần topdecls chỉ được phép ở mức trên cùng của mộtmodule Haskell, trong khi đó decls có thể được dùng ở cả phần đầu hoặc trong cácvùng lồng nhau (trong các khối let hoặc where)
Để dễ trình bày, ta chia phần khai báo thành 3 nhóm:
Kiểu dữ liệu do người dùng tự định nghĩa: chứa các khai báo về kiểu, kiểumới và dữ liệu (phần 4.2)
Trang 13 Các lớp kiểu và overloading: chứa các lớp, dẫn xuất và các khai báo chuẩnkhác (phần 4.3)
Các khai báo lồng nhau: chứa các kết nối dữ liệu, chữ ký kiểu và các khaibáo cố định (phần 4.4)
Haskell có một vài kiểu dữ liệu nguyên thủy (như kiểu số nguyên, kiểu dấu phẩyđộng), nhưng đa phần các kiểu dữ liệu được xây dựng với mã Haskell thôngthường sử dụng các khai báo kiểu và dữ liệu thông thường
2.8 Tổng quan về các kiểu và các lớp
Haskell sử dụng hệ thống kiểu đa hình Hindley-Milner truyền thống để đưa ra ngữ
nghĩa kiểu tĩnh, nhưng hệ thống kiểu này được mở rộng thành các kiểu lớp (haycác lớp) cung cấp một cấu trúc để đưa ra các hàm viết chồng nhau
Một khai báo lớp đưa ra một kiểu lớp mới và các thao tác viết chồng Bất kỳ dẫnxuất nào của lớp cũng phải hỗ trợ đầy đủ các thao tác này Một khai báo dẫn xuấtchỉ ra một kiểu là dẫn xuất của một lớp, chứa các định nghĩa của các thao tác đượcviết chồng- gọi là các phương thức lớp- được kế thừa từ các kiểu đã được đặt tên
Ví dụ: Giả sử ta cần viết chồng cho các thao tác (+) và negate với loại Int vàFloat Ta đưa ra 1 loại lớp mới gọi là Num:
Khai báo này được đọc là: “1 kiểu a là một dẫn xuất của lớp Num nếu tồn tại cácphương thức lớp (+) và negate cùng loại và được định nghĩa trên đó”
Trang 14Ta cũng có thể khai báo Int và Float là các dẫn xuất của lớp này.
Với addInt, negateInt, addFloat, negate Float được cho trong trường hợp này làcác hàm nguyên thủy, nhưng trong trường hợp tổng quát thì chúng có thể là cáchàm được người dùng định nghĩa Khai báo dẫn xuất đầu tiên được hiểu là “Int làmột dẫn xuất của lớp Num với những định nghĩa sau cho phương thức (+) vànegate”
2.8.1 Các loại
Để đảm bảo tính đúng đắn của các biểu thức kiểu, chúng được chia thành các loạikhác nhau có dạng:
Ký hiệu * biểu diễn loại các khởi tạo kiểu rỗng
Nếu k1 và k2 là các loại thì k1 k2 là loại của các kiểu có kiểu thuộc loại
k1 và trả về kiểu thuộc loại k2
Tham chiếu loại kiểm tra tính đúng đắn của biểu thức kiểu và giá trị kiểu Tuynhiên, không giống như kiểu, loại hoàn toàn ngầm định và không thấy được trongngôn ngữ
Trang 152.8.2 Cú pháp của kiểu
Đây là cú pháp cho biểu thức kiểu của Haskell Các giá trị kiểu được xây dựng từcác khởi tạo kiểu Các khởi tạo kiểu bắt đầu bởi các chữ viết hoa Dạng chính củacác biểu thức kiểu như sau:
1 Biến kiểu: được viết như các định danh với các chữ viết thường Loại của 1biến được ngầm xác định trong ngữ cảnh mà nó xuất hiện
2 Khởi tạo kiểu: Hầu hết các khởi tạo kiểu được viết như các định danh với chữviết hoa ở đầu Ví dụ:
Char, Int, Integer, Float, Double và Bool là các hằng số kiểu với loại *
Maybe và IO là các khởi tạo kiểu và được xem như các kiểu với loại * *
Khởi tạo data T … hay newtype T … thêm khởi tạo kiểu T vào danh sáchcác kiểu Loại của T được xác định bởi các tham chiếu loại
3 Ứng dụng kiểu: nếu t1 là một kiểu của loại k1 k2 và t2 là một kiểu của loại
k1 thì t1, t2 là một biểu thức kiểu thuộc loại k2
4 Một kiểu trong ngoặc có dạng (t) là đồng nhất với kiểu t
Ví dụ: biểu thức kiểu IO a có thể được hiểu là ứng dụng của một hằng số IO vớibiến a Vì khởi tạo kiểu của IO có loại * * nên biến a và cả biểu thức IO a đều
Trang 16có loại * Nói chung, ta cần tham chiếu loại để xác định loại thích hợp cho cáckiểu dữ liệu hay lớp mà người dùng tự định nghĩa.
Các cú pháp đặc biệt cho phép các biểu thức kiểu thể hiện dưới dạng truyềnthống:
1 Một kiểu hàm có dạng t1 t2, tương đương với kiểu () t1 t2 Ví dụ
Int Int Float có nghĩa là Int (Int Float)
2 Một kiểu bộ có dạng (t1,…, tk) với k lớn hơn 2 tương đương với (,…,) t1 …
tk chỉ kiểu bộ k với thành phần đầu tiên kiểu t1, thành phần thứ hai kiểu t2…
3 Một kiểu danh sách có dạng [t] tương đương với kiểu [ ] t, chỉ ra kiểu danhsách các thành phần kiểu t
để biểu diễn một ngữ cảnh và ta viết cx => t để chỉ ra kiểu t nằm trong ngữ cảnh
cx Ngữ cảnh cx chỉ được chứa các biến kiểu được tham chiếu trong t Để thuậntiện, ta viết cx => t ngay cả khi cx là rỗng
Trang 172.8.4 Ngữ nghĩa của kiểu và lớp
Hệ thống kiểu của Haskell luôn gán một kiểu cho mỗi biểu thức trong chươngtrình Trong trường hợp tổng quát, một kiểu có dạng ∀u cx. ⇒t với ulà tập của
các biến kiểu u1,…,un Trong các kiểu này, bất ky biến kiểu định lượng toàn cụcnào tự do trong cx cũng tự do trong t Dưới đây là một số ví dụ về kiểu:
Trong kiểu thứ ba, ràng buộc Eq (f a) không thể đơn giản hơn vì f là định lượngtoàn cục
Kiểu của biểu thức e phụ thuộc vào môi trường kiểu - nơi cung cấp kiểu chocác biến tự do trong e, và môi trường lớp- nơi khai báo kiểu nào là dẫn xuất củalớp nào (một kiểu trở thành một dẫn xuất của một lớp chỉ khi tồn tại một khai báodẫn xuất hay một mệnh đề kế thừa)
Kiểu tổng quát nhất có thể gán cho một biểu thức cụ thể (trong một môi trườngcho trước) gọi là kiểu chính Hệ thống kiểu mở rộng Hindley-Milner của Haskell
có thể suy ra kiểu chính của tất cả các biểu thức, bao gồm cả những phương thứclớp được viết chồng
3 Giá trị, kiểu và các khái niệm
Vì Haskell là một ngôn ngữ lập trình hàm thuần túy nên tất cả các tính toán đềuđược thực hiện thông qua việc định trị cho các biểu thức (các khái niệm mang tính
cú pháp) để sinh ra các giá trị (các thực thể trừu tượng mà ta coi là kết quả) Mọigiá trị đều có một kiểu gắn với nó (Bằng trực giác ta có thể hình dung kiểu như làtập các giá trị) Các ví dụ của biểu thức chứa các giá trị nguyên tử như số nguyên
5, ký tự ‘a’, và hàm \x x+1, và các giá trị có cấu trúc như danh sách [1,2,3] vàcặp (‘b’,4)
Trang 18Tương tự như biểu thức có giá trị, biểu thức kiểu có giá trị kiểu Ví dụ về biểuthức kiểu chứa các kiểu nguyên tử như kiểu Integer, Char, Integer Integer, vàkiểu có cấu trúc [Integer], cặp (Char, Integer).
Tất cả các giá trị trong Haskell đều là “first-class”- chúng có thể được truyềnnhư là các đối số của các hàm, các kiểu trả về, trong các cấu trúc… Ngược lại, cáckiểu trong Haskell lại không phải là “first-class” Kiểu là để mô tả giá trị và việcgắn kết một giá trị với kiểu của nó gọi là định kiểu Với các ví dụ về giá trị và kiểu
ở trên, ta có thể viết quá trình định kiểu như sau:
Ký hiệu “: :” được đọc là “có kiểu”
Các hàm trong Haskell được định nghĩa là một dãy các phương trình Ví dụ hàminc có thể được định nghĩa bởi một phương trình như sau:
Một phương trình là ví dụ của một khai báo Một kiểu khác của khai báo là khaibáo chữ ký kiểu Ta có thể khai báo việc định kiểu theo cách hiện của hàm inc nhưsau:
Về mặt học thuật, khi ta muốn chỉ ra rằng một biểu thức e1 có giá trị hay “giảmthành” biểu thức khác hay giá trị e2 , ta viết:
Trang 19nhầm kiểu Ví dụ như chúng ta không thể cộng hai ký tự với nhau, vì thế biểu thức
‘a’+’b’ là sai về kiểu Ưu điểm chính của các ngôn ngữ kiểu tĩnh đều được biếtđến: đó là tất cả các lỗi về kiểu được phát hiện trong quá trình biên dịch Khôngphải mọi lỗi đều do hệ thống kiểu Một biểu thức, ví dụ như 1/0, có thể định kiểuđược nhưng việc định trị cho nó sẽ sinh ra lỗi trong quá trình thực thi Hệ thốngkiểu luôn tìm thấy nhiều lỗi chương trình trong quá trình biên dịch giúp ích chongười lập trình và đồng thời cho phép trình biên dịch tạo ra những mã hiệu quảhơn (ví dụ như không cần quan tâm đến các lỗi về kiểu khi thực thi)
Hệ thống kiểu cũng đảm bảo rằng các chữ ký kiểu do người dùng cung cấp làchính xác.Trên thực tế, hệ thống kiểu của Haskell đủ mạnh để để cho phép chúng
ta không cần viết bất kỳ một chữ ký kiểu nào; ta có thể nói rằng hệ thống kiểu nàysuy dẫn ra kiểu chính xác Tuy nhiên việc thay thế chữ ký kiểu cho hàm inc là một
ý tưởng hay, bởi lẽ chữ ký kiểu là một kiểu chú thích hiệu quả, giúp ích nhiều choviệc debug sau này
3.1 Kiểu đa hình
Haskell có các kiểu đa hình- các kiểu có thể được định lượng linh hoạt theo nhiềukiểu khác nhau Các biểu thức có kiểu đa hình được mô tả như một họ các kiểu Ví
dụ ∀a a[ ]là họ các kiểu (với mỗi a) chứa kiểu danh sách của a Danh sách các số
nguyên (ví dụ [1,2,3]), danh sách các ký tự ([‘a’,’b’,’c’]), thậm chí là danh sáchcủa các danh sách số nguyên… là thành phần của họ này (Chú ý rằng [2,’b’]không phải là một ví dụ hợp lệ vì không có kiểu đơn nào chứ cả 2 và ‘b’)
Danh sách là cấu trúc dữ liệu được dùng phổ biến trong các ngôn ngữ lập trìnhhàm và là phương tiện để diễn giải ý nghĩa của tính đa hình Danh sách [1,2,3]trong Haskell thực chất là viết tắt của danh sách 1:( 2:( 3:([])) với [] là danh sáchrỗng và : là toán tử trung tố làm nhiệm vụ cộng tham số đầu tiên vào phía trướctham số thứ hai (1 danh sách) Ta cũng có thể viết như sau: 1:2:3:[]
Trang 20Lấy một ví dụ về một hàm được định nghĩa bởi người dùng làm việc trên danhsách: tính số phần tử trong danh sách:
Ta có thể hiểu hàm này như sau: “Độ dài của một danh sách rỗng là 0 và độ dàicủa một danh sách có phần tử đầu x và phần còn lại là xs là 1 cộng với độ dài củaxs”
Mặc dù ví dụ này khá hiển nhiên nhưng nó đề cập đến một khái niệm mới: khớpcác mẫu Vế trái của phương trình chứa các mẫu như [], x:xs Trong một ứng dụnghàm, các mẫu này được khớp với các tham số thực theo cách khá trực quan ([] chỉkhớp với danh sách rỗng, x:xs khớp với danh sách có tối thiểu 1 thành phần và gán
x với phần tử đầu, xs với phần còn lại) Khi quá trình khớp này thành công, vếphải sẽ được định trị và nếu tất cả các phương trình không đúng sẽ có lỗi xảy ra.Định nghĩa các hàm thông qua việc khớp các mẫu là khá phổ biến trong Haskell
và người dùng phải làm quen với rất nhiều mẫu cho phép Hàm xác định độ dàidưới đây là một ví dụ về hàm đa hình Nó có thể áp dụng cho một danh sách chứacác thành phần có bất kỳ kiểu nào, ví dụ [Integer], [Char] hay [[Integer]]
Dưới đây là hai ví dụ nữa về hàm đa hình Hàm head trả về thành phần đầu củadanh sách và hàm tail trả về các thành phần của danh sách sau phần tử đầu tiên
Không giống hàm length, các hàm này không định nghĩa cho mọi trường hợp cóthể xảy ra của đối số vào Lỗi thực thi sẽ xảy ra khi áp dụng hàm này với một danhsách rỗng
Trang 21Với các kiểu đa hình, ta có thể thấy một số kiểu là tổng quát hơn các kiểu kháctheo nghĩa là tập giá trị mà chúng định nghĩa lớn hơn Ví dụ, kiểu [a] tổng quáthơn kiểu [Char] Nói cách khác, kiểu [Char] có thể được kế thừa từ kiểu [a] bằngcách thay thế a thích hợp Hệ thống kiểu của Haskell có hai tính chất quan trọng:
Đầu tiên, mọi biểu thức được định kiểu đảm bảo chỉ có một kiểu chính duynhất
Sau đó, kiểu chính được suy dẫn tự động
So sánh với ngôn ngữ chỉ có kiểu đơn hình như C, người đọc sẽ thấy tính đahình sẽ diễn tả diễn cảm hơn, và sự suy dẫn kiểu sẽ làm giảm gánh nặng về kiểucho người lập trình
Kiểu chính của một biểu thức hay một hàm là kiểu ít tổng quát nhất thỏa mãn
“chứa tất cả các dẫn xuất của biểu thức” Ví dụ như kiểu chính của head là [a]a;ba, aa hoặc thậm chí là a nhưng quá tổng quát hoặc [Integer][Integer] làquá cụ thể Sự tồn tại duy nhất của các kiểu là tính chất của hệ thống kiểuHindley-Milner, nền tảng của hệ thống kiểu trong Haskell, ML, Miranda và hầuhết các ngôn ngữ lập trình hàm khác
3.2 Các kiểu do người dùng tự định nghĩa
Ta có thể tự định nghĩa các kiểu riêng trong Haskell với các khai báo dữ liệu Mộtkiểu quan trọng đã được định nghĩa trước trong Haskell là giá trị chân lý:
Kiểu được định nghĩa ở đây là Bool và nó có chính xác 2 giá trị: True và False.Kiểu Bool là một ví dụ của khởi tạo kiểu True và False là các khởi tạo dữ liệu(hay nói ngắn gọn là khởi tạo)
Tương tự như vậy ta có thể định nghĩa một kiểu Color:
Trang 22Cả Bool và Color đều là các ví dụ về các kiểu liệt kê, chúng chứa một số lượngxác định các khởi tạo dữ liệu.
Đây là ví dụ về một kiểu chỉ có một khởi tạo dữ liệu:
Do chỉ có một khởi tạo, kiểu giống như Point được gọi là kiểu bộ bởi vì bản chấtcủa nó là tích Đề-các của các kiểu khác Ngược lại, các kiểu có nhiều khởi tạo nhưBool và Color được gọi là kiểu tổng hay kiểu hợp
Quan trọng hơn là Point là ví dụ về kiểu đa hình: với mọi kiểu t, nó định nghĩakiểu các điểm Đề-các nhận t là kiểu cơ sở Kiểu Point có thể được thấy như mộtkhởi tạo kiểu vì kiểu t tạo nên kiểu mới là Point t (Cũng như vậy, sử dụng ví dụ
về danh sách ở trên, [] cũng là một khởi tạo kiểu Với một kiểu t bất kỳ, ta có thể
“áp dụng” [] để tạo ra kiểu mới [t] Cú pháp của Haskell cho phép viết [] t thành[t] Tương tự, là một khởi tạo kiểu: cho 2 kiểu t và u, tu là kiểu của các hàmánh xạ các thành phần thuộc kiểu t thành các thành phần thuộc kiểu u)
Chú ý rằng kiểu của khởi tạo dữ liệu nhị phân Pt là aaPoint a, vì vậy cácđịnh kiểu sau là đúng:
Mặt khác, biểu thức Pt ‘a’ 1 là không đúng vì ‘a’ và 1 là khác kiểu Cần phải phânbiệt được giữa áp dụng khởi tạo dữ liệu để sinh ra giá trị và áp dụng khởi tạo kiểu
để sinh ra kiểu Áp dụng khởi tạo dữ liệu để sinh ra giá trị xảy ra ở quá trình thựchiện và đó là cách ta tính toán trong Haskell Trong khi đó, áp dụng khởi tạo kiểu
để sinh ra kiểu xảy ra trong quá trình biên dịch và là một phần của quá trình xử lýcủa hệ thống kiểu để đảm bảo an toàn kiểu
Khởi tạo kiểu như Point và khởi tạo dữ liệu như Pt nằm trong các không giantên khác nhau Điều này cho phép khởi tạo kiểu và khởi tạo dữ liệu như sau:
Trang 23Kiểu đệ quy: Các kiểu cũng có thể có tính chất đệ quy như trong kiểu củacây nhị phân:
Ta đã định nghĩa một kiểu cây nhị phân đa hình với các thành phần là nút lá chứagiá trị của kiểu a, một nút trong (các nhánh) chứa (đệ quy) 2 cây con
Khi đọc các khai báo dữ liệu, cần nhớ rằng Tree là 1 khởi tạo kiểu, trong khi đóBranch và Leaf là các khởi tạo dữ liệu Ngoài việc đưa ra các kết nối giữa các khởitạo, khai báo trên định nghĩa các kiểu sau:
Với ví dụ này, ta đã định nghĩa một kiểu đủ mạnh để định nghĩa một số hàm đệquy sử dụng nó Giả sử ta muốn định nghĩa hàm fringe trả về danh sách các phần
tử tại nút lá của cây từ trái qua phải Trong trường hợp này, ta thấy kiểu của hàm
là Tree a[a]: hàm đa hình mà với mỗi kiểu a, ánh xạ cây a thành một danh sách
a Ta có thể định nghĩa như sau:
Ở đây ++ là toán tử trung tố giúp ghép 2 danh sách Cũng giống ví dụ về hàmlength ở trên, hàm fringe được định nghĩa dựa trên việc khớp mẫu với các khởi tạo
do người dùng định nghĩa: Leaf và Branch
3.3 Kiểu đồng nghĩa
Để thuận tiện khi làm việc, Haskell cung cấp cách định nghĩa các kiểu đồng nghĩa.Các kiểu đồng nghĩa này được tạo ra bởi các khai báo kiểu Dưới đây là một số vídụ:
Trang 24Các kiểu đồng nghĩa này không định nghĩa các kiểu mới mà chỉ đơn giản đặt tênmới cho các kiểu đã có Ví dụ kiểu Person Name hoàn toàn tương đương với(String, Address)String Ngoài ưu điểm về tính ngắn gọn của tên mới so với tên
cũ, chúng còn cung cấp tính dễ đọc, dễ nhớ cho chương trình Ta cũng có thể đặttên mới cho các kiểu đa hình:
Đây là kiểu danh sách kết hợp: ghép các giá trị thuộc kiểu a với các giá trị thuộckiểu b
3.4 Các kiểu được xây dựng sẵn
Trong phần trước, ta đã giới thiệu một số kiểu được xây dựng sẵn trong Haskellnhư danh sách, kiểu bộ, kiểu số nguyên, và kiểu ký tự Ta cũng đã chỉ ra cách địnhnghĩa các kiểu mới cho người sử dụng Bên cạnh các cú pháp đặc biệt, một câu hỏiđặt ra là các kiểu được xây dựng sẵn này có đặc biệt hơn các kiểu do người dùngđịnh nghĩa? Câu trả lời là KHÔNG Các cú pháp đặc biệt không mang nhiều ngữnghĩa mà chỉ là các quy ước mang lại tính thuận tiện và nhất quán
Ta có thể nghiên cứu cách thức định nghĩa các kiểu có sẵn dựa trên cú pháp đặcbiệt Ví dụ, kiểu Char có thể được viết như sau:
Kiểu khai báo này không đúng về mặt cú pháp, ta cần phải sửa lại như sau:
Mặc dù khởi tạo này là chính xác hơn nhưng lại không thuận tiện để biểu diễn các
ký tự
Trang 25Trong mọi trường hợp, các mã “pseudo-Haskell” như trên giúp ta hiểu rõ hơncác cú pháp đặc biệt của Haskell Cụ thể là ta có thể thấy Char là kiểu liệt kê chứarất nhiều các khởi tạo Theo cách hiểu này, ta có thể khớp mẫu các ký tự khi địnhnghĩa một hàm.
Ví dụ sau đây cũng chỉ ra cách sử dụng các chú thích trong Haskell như đã trìnhbày ở trên Ta định nghĩa kiểu Int và Integer như sau:
Kiểu Int có mức độ liệt kê lớn hơn kiểu Char nhưng vẫn rất rõ ràng! Mặc dù vậy,pseudo-code cho kiểu Integer lại là kiểu liệt kê không xác định:
Tương tự như vậy, kiểu bộ cũng có thể được định nghĩa:
Mỗi định nghĩa trên khai báo một kiểu bộ với chiều dài cụ thể với dấu (…) đóngvai trò trong cú pháp biểu thức (là khởi tạo dữ liệu) và cú pháp biểu thức kiểu (làkhởi tạo kiểu) Các dấu chấm dọc sau định nghĩa cuối cùng chỉ ra tính không xácđịnh của các khai báo
Thực tế, danh sách rất dễ làm việc và có thể được định nghĩa:
[] là danh sách rỗng, “:” là khởi tạo danh sách trung tố, do đó [1,2,3] tương đươngvới 1:2:3:[] Kiểu của [] là a và kiểu của : là a ->[a]->[a]
Ở đây ta cần phân biệt giữa kiểu bộ và kiểu danh sách Kiểu danh sách có bảnchất đệ quy với các thành phần đồng nhất như nhau và chiều dài là bất kỳ Kiểu bộ
có bản chất không đệ quy với các thành phần hỗn hợp và chiều dài cố định Quytắc định kiểu cho bộ và danh sách như sau:
Với (e1, e2,…,en), nếu e1 có kiểu ti thì kiểu của bộ là (t1, t2,…,tn)
Với [e1, e2,…,en], nếu ei có kiểu t thì kiểu của danh sách [t]
Trang 263.4.1 Thể hiện danh sách và chuỗi số học
Tương tự như Lisp, danh sách có mặt khắp mọi nơi trong Haskell nên cách làmviệc với nó cũng đa dạng Ngoài cách khởi tạo danh sách đã trình bày ở trên,Haskell cung cấp một phương thức gọi là thể hiện danh sách, ví dụ:
Biểu diễn này có ý nghĩa: “danh sách tất cả các f x sao cho x được lấy từ xs”
là điều kiện sinh Haskell cho phép nhiều điều kiện sinh trong một thểhiện danh sách:
Thể hiện danh sách này là tích Đề-các của 2 danh sách xs và ys Các thành phầnđược chọn theo các điều kiện sinh lần lượt từ trái qua phải Ví dụ như nếu ta có xs
là [1,2] và ys là [3,4] thì kết quả là [(1,3),(1,4),(2,3),(2,4)]
Bên cạnh các điều kiện sinh, Haskell cho phép sử dụng các biểu thức logic gọi
là các đảm bảo (guard) Các đảm bảo đặt các ràng buộc lên các thành phần đượcsinh ra Ví dụ dưới đây là định nghĩa về thuật toán sắp xếp khá quen thuộc:
Nhằm hỗ trợ cho việc sử dụng danh sách,Haskell có các cú pháp đặc biệt chocác chuỗi số học Ta xét các ví dụ sau:
3.4.2 Chuỗi
Ví dụ dưới đây chỉ ra một kiểu đã được xây dựng sẵn trong Haskell với chú ý rằngchuỗi “hello” là dạng viết tắt của danh sách các ký tự [‘h’,’e’,’l’,’l’,’o’]
Trang 274 Hàm trong Haskell
Vì Haskell là ngôn ngữ lập trình hàm, hàm (fucntion) là có một trong những thànhphần chính của Haskell Phần này sẽ trình bày những vấn đề liên quan đến hàmtrong Haskell
Trước hết xem ví dụ về định nghĩa hàm tính tổng của 2 số đầu vào:
Dạng hàm như trên được gọi là curried function (được đặt theo tên của HaskellCurry), và phân biệt với các hàm dạng uncurried function Chẳng hạn hàmuncurried của hàm tính tổng 2 số có dạng:
add (x, y) = x + y
Hàm tính tổng add x y có thể được viết dưới dạng (add x) y, trong đó về thựcchất hàm add giống như hàm một đối số Việc thực hiện add x tạo ra một hàmmới, hàm này được sử dụng cho đối số thứ 2 Chẳng hạn như hàm inc được sinh ranhư sau:
Kiểu (type) của hàm add được biểu diễn: Integer Integer Integer, hay cóthể được viết lại dưới dạng: Integer (Integer Integer) Ngoài cách sử dụngcurried function để thu được kết quả trả về là một hàm, curried function còn được
sử dụng trong định nghĩa những hàm mà sử dụng tham số là hàm:
Hàm map là hàm thực hiện biến đổi f trên mảng đầu vào và cho kết quả là mộtmảng, trong đó f là hàm đầu vào Một ví dụ về sử dụng hàm map như sau:
trong đó thực hiện phép toán cộng 1 đối với tất cả phần tử của mảng đầu vào