Sự khác biệt chính giữa phân tích cú pháp đơn và ứng dụng là cách xử lý thành phần tuần tự. Trong trường hợp của một trình phân tích cú pháp, chúng tôi sử dụng (<*>)
, trong khi với một đơn nguyên, chúng tôi sử dụng (>>=)
.
(<*>) :: Parser (a -> b) -> Parser a -> Parser b
(>>=) :: Parser a -> (a -> Parser b) -> Parser b
Cách tiếp cận đơn giản linh hoạt hơn, vì ngữ pháp của phần thứ hai phụ thuộc vào kết quả đầu tiên, nhưng chúng tôi hiếm khi cần thêm tính linh hoạt trong thực tế.
Bạn có thể nghĩ rằng có một số tính linh hoạt bổ sung không thể làm tổn thương, nhưng thực tế nó có thể. Nó ngăn cản chúng tôi làm phân tích tĩnh hữu ích trên một trình phân tích cú pháp mà không cần chạy nó. Ví dụ: giả sử chúng tôi muốn biết liệu trình phân tích cú pháp có thể khớp với chuỗi trống hay không và những ký tự đầu tiên có thể có trong kết quả trùng khớp. Chúng tôi muốn chức năng
empty :: Parser a -> Bool
first :: Parser a -> Set Char
Với trình phân tích cú pháp, chúng tôi có thể dễ dàng trả lời câu hỏi này. (Tôi đang gian lận một chút ở đây. Hãy tưởng tượng chúng tôi có một nhà xây dựng dữ liệu tương ứng với (<*>)
và (>>=)
trong phân tích cú pháp ứng cử viên của chúng tôi "ngôn ngữ").
empty (f <*> x) = empty f && empty x
first (f <*> x) | empty f = first f `union` first x
| otherwise = first f
Tuy nhiên, với trình phân tích cú pháp đơn thuần, chúng tôi không biết ngữ pháp của phần thứ hai là gì mà không biết đầu vào.
empty (x >>= f) = empty x && empty (f ???)
first (x >>= f) | empty x = first x `union` first (f ???)
| otherwise = first x
Bằng cách cho phép nhiều hơn, chúng tôi có thể giải thích ít hơn. Điều này tương tự như sự lựa chọn giữa các hệ thống kiểu động và tĩnh.
Nhưng điểm này là gì? Chúng ta có thể sử dụng kiến thức tĩnh bổ sung này để làm gì? Ví dụ, chúng ta có thể sử dụng nó để tránh backtracking trong LL (1) phân tích cú pháp bằng cách so sánh các ký tự tiếp theo để first
thiết lập của mỗi thay thế. Chúng tôi cũng có thể xác định tĩnh cho dù điều này có thể mơ hồ hay không bằng cách kiểm tra xem first
có hai lựa chọn thay thế trùng lặp hay không.
Ví dụ khác là nó có thể được sử dụng để phục hồi lỗi, như được hiển thị trong giấy Deterministic, Error-Correcting Combinator Parsers bởi S. Doaitse Swierstra và Luc Duponcheel.
Thông thường, tuy nhiên, sự lựa chọn giữa phân tích cú pháp ứng dụng và đơn lẻ đã được thực hiện bởi các tác giả của thư viện phân tích mà bạn đang sử dụng. Khi một thư viện như Parsec trưng ra cả hai giao diện, sự lựa chọn cái nào để sử dụng hoàn toàn là một phong cách. Trong một số trường hợp, mã ứng dụng dễ đọc hơn mã đơn điệu và đôi khi nó là một cách khác.
Đợi đã! Tôi đã nghĩ như vậy cho đến ngày hôm nay, khi nó xảy ra với tôi rằng thử nghiệm 'trống rỗng 'cũng có thể được áp dụng cho các trình phân tích cú pháp đơn thuần. Lý do là chúng ta có thể lấy giá trị bạn đặt tên '???' bằng cách áp dụng trình phân tích cú pháp 'x' trên chuỗi rỗng. Nói chung, bạn chỉ có thể nạp chuỗi rỗng vào trình phân tích cú pháp và xem điều gì xảy ra. Tương tự như vậy, tập hợp các ký tự đầu tiên có thể thu được ít nhất trong một dạng chức năng 'đầu tiên :: Parser a -> (Char -> Bool)'. Tất nhiên, việc chuyển đổi sau thành 'Set Char' sẽ liên quan đến việc đếm các ký tự không hiệu quả, đó là nơi các functors ứng dụng có cạnh. –
@HeinrichApfelmus Bạn không thể nhận được câu trả lời cho 'rỗng' theo cách đó. Hoặc bạn * có thể *, nhưng nó giống như đưa ra câu trả lời cho [http://en.wikipedia.org/wiki/Halting_problem] với "cho phép chạy chương trình và xem nó có dừng lại không". – phadej
@hammar: nếu chúng ta chạy 'let x = pure f <*> y <*> x trong rỗng x'. Nếu 'empty y' là' False', thì tính toán không chấm dứt ... chỉ để chỉ ra, rằng nó không đơn giản như vậy. – phadej