Chà, nếu bạn nhìn sâu hơn một chút, cả hai thực sự cũng bao gồm các mảng trong ngôn ngữ cơ sở:
- Báo cáo lược đồ sửa đổi lần thứ 5 (R5RS) bao gồm loại vectơ , là các bộ sưu tập được lập chỉ mục số nguyên có kích thước cố định với thời gian tuyến tính tốt hơn để truy cập ngẫu nhiên.
- Báo cáo Haskell 98 cũng có một kiểu mảng .
Tuy nhiên, hướng dẫn lập trình chức năng từ lâu đã nhấn mạnh các danh sách liên kết đơn trên các mảng hoặc danh sách liên kết đôi. Trên thực tế, có khả năng quá cao. Có một số lý do cho nó, tuy nhiên.
Đầu tiên là danh sách liên kết đơn là một trong những kiểu dữ liệu đệ quy đơn giản nhất nhưng hữu ích nhất. Có thể định nghĩa tương đương do người dùng xác định loại danh sách của Haskell như sau:
data List a -- A list with element type `a`...
= Empty -- is either the empty list...
| Cell a (List a) -- or a pair with an `a` and the rest of the list.
Thực tế là các danh sách là một kiểu dữ liệu đệ quy có nghĩa là các hàm hoạt động trong danh sách thường sử dụng đệ quy cấu trúc . Theo thuật ngữ Haskell: bạn khớp mẫu trên các hàm tạo danh sách và bạn lặp lại trên một phần con của danh sách. Trong hai định nghĩa hàm cơ bản này, tôi sử dụng biến as
để chỉ phần đuôi của danh sách. Vì vậy, lưu ý rằng các cuộc gọi đệ quy "xuống" xuống danh sách:
map :: (a -> b) -> List a -> List b
map f Empty = Empty
map f (Cell a as) = Cell (f a) (map f as)
filter :: (a -> Bool) -> List a -> List a
filter p Empty = Empty
filter p (Cell a as)
| p a = Cell a (filter p as)
| otherwise = filter p as
Kỹ thuật này đảm bảo rằng chức năng của bạn sẽ chấm dứt cho tất cả các danh sách hữu hạn, và cũng là một kỹ thuật giải quyết vấn đề tốt, nó có xu hướng tự nhiên phân tách các vấn đề thành các phần con đơn giản hơn, có thể sử dụng hơn.
Vì vậy, danh sách liên kết đơn có lẽ là loại dữ liệu tốt nhất để giới thiệu cho sinh viên về các kỹ thuật này, rất quan trọng trong lập trình chức năng.
Lý do thứ hai ít hơn là lý do "tại sao danh sách liên kết đơn", nhưng lý do "tại sao không phải là danh sách hoặc mảng liên kết kép": những kiểu dữ liệu sau này thường gọi là đột biến (biến có thể sửa đổi), mà lập trình chức năng rất thường xuyên tránh xa Vì vậy, khi nó xảy ra:
- Trong một ngôn ngữ háo hức như Scheme, bạn không thể tạo một danh sách liên kết đôi mà không sử dụng đột biến.
- Trong một ngôn ngữ lười biếng như Haskell, bạn có thể tạo một danh sách liên kết đôi mà không cần sử dụng đột biến. Nhưng bất cứ khi nào bạn tạo một danh sách mới dựa trên danh sách đó, bạn buộc phải sao chép hầu hết nếu không phải là tất cả các cấu trúc của bản gốc. Trong khi với các danh sách liên kết đơn, bạn có thể viết các hàm sử dụng danh sách "chia sẻ cấu trúc" có thể sử dụng lại các ô của danh sách cũ khi thích hợp.
- Theo truyền thống, nếu bạn sử dụng mảng theo cách không thay đổi, điều đó có nghĩa là mỗi lần bạn muốn sửa đổi mảng, bạn phải sao chép toàn bộ. (
vector
Tuy nhiên, các thư viện Haskell gần đây đã tìm thấy các kỹ thuật cải thiện đáng kể vấn đề này).
Lý do thứ ba và cuối cùng áp dụng cho các ngôn ngữ lười biếng như Haskell là chủ yếu: các danh sách liên kết đơn lười biếng, trong thực tế, thường giống với các trình lặp hơn là các danh sách trong bộ nhớ phù hợp. Nếu mã của bạn đang tiêu thụ các yếu tố của danh sách một cách tuần tự và ném chúng ra khi bạn đi, mã đối tượng sẽ chỉ cụ thể hóa các ô danh sách và nội dung của nó khi bạn bước qua danh sách.
Điều này có nghĩa là toàn bộ danh sách không cần tồn tại trong bộ nhớ cùng một lúc, chỉ có ô hiện tại. Các ô trước ô hiện tại có thể được thu gom rác (điều này không thể thực hiện được với danh sách liên kết đôi); các ô muộn hơn các ô hiện tại không cần phải tính toán cho đến khi bạn đến đó.
Nó còn đi xa hơn thế. Có kỹ thuật được sử dụng trong một số thư viện Haskell phổ biến, được gọi là fusion , trong đó trình biên dịch phân tích mã xử lý danh sách của bạn và phát hiện các danh sách trung gian đang được tạo và tiêu thụ tuần tự và sau đó "vứt đi". Với kiến thức này, trình biên dịch có thể loại bỏ hoàn toàn việc cấp phát bộ nhớ cho các ô của danh sách đó. Điều này có nghĩa là một danh sách liên kết đơn trong chương trình nguồn Haskell, sau khi biên dịch, thực sự có thể bị biến thành một vòng lặp thay vì cấu trúc dữ liệu.
Fusion cũng là kỹ thuật mà vector
thư viện nói trên sử dụng để tạo mã hiệu quả cho các mảng không thay đổi. Điều tương tự cũng xảy ra với các thư viện cực kỳ phổ biến bytestring
(mảng byte) và text
(chuỗi Unicode), được xây dựng để thay thế cho String
kiểu bản địa không quá lớn của Haskell (giống như [Char]
danh sách ký tự liên kết đơn). Vì vậy, trong Haskell hiện đại, có một xu hướng mà các kiểu mảng bất biến với sự hỗ trợ nhiệt hạch đang trở nên rất phổ biến.
Danh sách hợp nhất được tạo điều kiện bởi thực tế là trong một danh sách liên kết đơn, bạn có thể đi tiếp nhưng không bao giờ lùi . Điều này mang đến một chủ đề rất quan trọng trong lập trình chức năng: sử dụng "hình dạng" của kiểu dữ liệu để lấy "hình dạng" của một tính toán. Nếu bạn muốn xử lý các phần tử một cách tuần tự, một danh sách liên kết đơn là một kiểu dữ liệu, khi bạn sử dụng nó với đệ quy cấu trúc, sẽ cung cấp cho bạn mẫu truy cập đó rất tự nhiên. Nếu bạn muốn sử dụng chiến lược "phân chia và chinh phục" để tấn công một vấn đề, thì cấu trúc dữ liệu cây có xu hướng hỗ trợ rất tốt.
Rất nhiều người từ bỏ chương trình lập trình chức năng từ rất sớm, vì vậy họ được tiếp xúc với các danh sách liên kết đơn nhưng không tiếp cận với các ý tưởng cơ bản nâng cao hơn.