5. Biểu thức theo trường hợp và khớp mẫu
5.4. Mẫu Lazy
Có một loại mẫu khác được chấp nhận trong Haskell gọi là : mẫu Lazy, và có dạng
~pat. Mẫu Lazy là bất khả bác: so khớp một giá trị v với ~pad là luôn thành công, bất chấp pat. Nói theo ngôn ngữ thao tác, nếu một định danh trong pat được “sử dụng” sau trong vế phải, nó phải được ràng buộc vào phần giá trị đó, cái mà cho kết quả nếu v là so khớp thành công với pat và ⊥ nếu ngược lại.
Các mẫu Lazy hữu ích trong các trường hợp khi cá cấu trúc dữ liệu vô hạn được định nghĩa một cách đệ quy. Chẳng hạn, danh sách vô hạn là một phương tiện tuyệt vời để viết các chương trình mô phỏng, và trong trường hợp này, danh sách
vô hạn thường được gọi là các dòng. Xem xét một trường hợp đơn giản mô phỏng các tương tác giữa tiến trình máy chủ Server và tiến trình máy khác Client, trong đó Client gửi một chuỗi các yêu cầu requests tới Server, và Server trả lời mỗi yêu cầu với một kiểu response. Hoàn cảnh này được miêu tả một cách hình tượng như hình dưới.
Sử dụng dòng để mô phỏng dòng thông ðiệp, trong Haskell, mã tương ứng với sơ đồ này là:
reqs = client init resps
resps = server reqs
Những phương trình đệ quy này là sự chuyển ngữ trực tiếp từ sơ đồ. Chúng ta hãy giả thiết xa hơn rằng cấu trúc của Server và Client như sau:
client init (resp:resps) = init : client (next resp)
resps
server (req:reqs) = process req : server reqs
trong đó, ta giả sử rằng next là hàm, cho trước một đáp ứng từ Server, sẽ trả về yêu cầu tiếp theo, và process là hàm xử lý các yêu cầu từ phía client, trả về các đáp ứng thích hợp.
Nhưng thật không may, chương trình có một vấn đề nghiêm trọng : nó không xuất ra một đầu ra nào cả ! Vấn đề là client, vì sử dụng cài đặt đệ qui cho các reqs
và resp, sẽ cố gắng so khớp trên danh sách các đáp ứng trước khi nó gửi yêu cầu lên! Nói cách khác, việc khớp mẫu là được hoàn thành “quá sớm”. Một cách để sửa chữa vấn đề này là định nghĩa lại client như sau:
client init resps = init : client (next (head resps)) (tail resps)
Mặc dù chạy được, giải pháp này không sáng sủa bằng cái trước đó. Một giải pháp tốt hơn là sử dụng mẫu Lazy:
client init ~(resp:resps) = init : client (next resp) resps Vì các mẫu Lazy là bất khả bác, sự so khớp là ngay lập tức thành công, cho phép yêu cầu ban đầu được “gửi” lên, theo thứ tự từng cái cho phép đáp ứng đầu tiên được sinh ra.
Một ví dụ về hoạt động của chương trình trên như sau:
init = 0
next resp = resp
process req = req + 1
Với hàm take cho ra kết quả như sau:
take 10 reqs => [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
Một ví dụ khác của việc sử dụng các mẫu Lazy là trong định nghĩa Fibonacci ở phần trên:
fib = 1 : 1 : [a+b| (a, b) zip fib (tail fib)] Chúng ta viết lại biểu thức:
fib@(1: tfib) = 1 : 1 : [a+b, (a, b) zip fib tfib]
Cách viết này có ưu điểm hơn ở việc không phải sử dụng tail ở vế phải, do nó có thể tái sử dụng tfib đã có ở vế trái. Bây giờ, sử dụng cùng một lập luận như ở trên, nó cũng cho phép tin rằng chương trình sẽ không xuất ra một output nào. Nhưng lạ thay, nó lại xuất ra output, nguyên nhân rất đơn giản, trong Haskel, các ràng buộc mẫu (partern bindings) được xem là có một dấu ~ ở trước, phản ánh những hành vi phổ biến nhất của một ràng buộc mẫu (partern binding ). Như vậy, ta thấy rằng mẫu Lazy đóng một vai trò rất quan trọng trong Haskell.