2011-10-22 41 views

Trả lời

82

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 (<*>)(>>=) 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.

+5

Đợ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. –

+5

@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

+3

@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

4

Với Parsec lợi ích của việc sử dụng Applicative chỉ là phong cách. Monad có lợi thế là nó mạnh hơn - bạn có thể triển khai thực hiện phân tích ngữ cảnh nhạy cảm.

Phân tích UU của Doaitse Swierstra hiệu quả hơn nếu chỉ được sử dụng.

+0

báo cáo ISTR rằng chính thức, bởi vì Haskell cho phép văn phạm tiếng vô hạn, đơn nguyên không thực sự gia tăng số lượng ngôn ngữ dễ nhận biết. – luqui

+3

@luqui Tôi tò mò về bình luận của bạn. Đây là một ngôn ngữ. Bảng chữ cái là Haskell 'String', và ngôn ngữ là tập hợp các từ trong đó tất cả các chữ đều bằng nhau. Điều này dễ chết như một trình phân tích cú pháp đơn thuần: 'option [] (anyToken >> = nhiều. ExactToken)' (ở đây 'anyToken' và' exactToken' không thực sự là một phần của thư viện Parsec, nhưng có lẽ phải là; hỏi tôi nếu bạn không chắc chắn về những gì họ làm). Trình phân tích cú pháp ứng dụng tương ứng sẽ như thế nào? –

+2

@stephen, bạn có thể đưa ra tham chiếu cho các trình phân tích cú pháp nhạy cảm với ngữ cảnh không? Tôi tò mò sức mạnh chính xác của các trình phân tích cú pháp đơn thuần và ứng dụng là gì. – sdcvvc

2

Đơn vị quảng cáo hoàn toàn là trừu tượng chi tiết hơn so với Đơn đăng ký. Bạn có thể viết

instance (Monad m) => Applicative m where 
    pure = return 
    (<*>) = ap 

Nhưng không có cách nào để viết

instance (Applicative a) => Monad a where 
    return = pure 
    (>>=) = ??? 

Vì vậy, có, nó thực chất là một vấn đề của phong cách. Tôi tưởng tượng nếu bạn sử dụng returnap, thì phải có không bị mất hiệu suất khi sử dụng pure<*>. Vì ứng dụng là giao diện nhỏ hơn nghiêm ngặt so với Monad, điều này có nghĩa là <*> đôi khi có thể được tối ưu hóa cao hơn ap. (Nhưng với quy tắc viết lại GHC thông minh, người ta thường có thể đạt được cùng một sự tối ưu hóa bất kể.)

Phân tích đơn thuần có phải không?

Vì Monads là tập hợp con của các ứng dụng, tôi sẽ kết luận rằng phân tích cú pháp ứng dụng là tập con của phân tích cú pháp đơn thuần.

+0

Bạn có ý nói rằng Monads là một _superset_ của các ứng viên? – Guildenstern

+1

@Guildenstern Hoạt động đơn lẻ là một phần lớn các hoạt động Ứng dụng. Nhưng nói một cách khác: các kiểu có một thể hiện của 'Monad' là một tập con của các kiểu có một cá thể' Applicative'. Khi nói về "Monads" và "Applicatives", người ta thường đề cập đến các loại, không phải là các hoạt động. –

+0

"Tôi tưởng tượng nếu bạn sử dụng' return' và 'ap', thì sẽ không bị mất hiệu năng khi sử dụng' pure' và '<*>'. " IIRC không phải như vậy. Có rất nhiều tình huống (các trình phân tích cú pháp chỉ là một trong số chúng), trong đó '<*>' hoạt động tốt hơn 'ap'. – semicolon

10

Lý do chính tôi có thể thấy để thích phân tích cú pháp đơn trên trình phân tích cú pháp đơn thuần là lý do chính để ưu tiên mã đơn trên mã đơn lẻ trong bất kỳ ngữ cảnh nào: ít mạnh mẽ hơn, các ứng dụng đơn giản hơn.

Đây là ví dụ về nguyên tắc kỹ thuật tổng quát hơn: sử dụng công cụ đơn giản nhất để thực hiện công việc. Không sử dụng thang nâng khi một dolly sẽ làm. Không sử dụng cưa bàn để cắt các phiếu giảm giá. Đừng viết mã số IO khi nó có thể thuần khiết. Giữ nó đơn giản.

Nhưng đôi khi, bạn cần công suất bổ sung là Monad. Một dấu hiệu chắc chắn của điều này là khi bạn cần phải thay đổi quá trình tính toán dựa trên những gì đã được tính toán cho đến nay. Trong phân tích cú pháp, điều này có nghĩa là xác định cách phân tích cú pháp những gì tiếp theo dựa trên những gì đã được phân tích cú pháp cho đến nay; nói cách khác bạn có thể xây dựng ngữ pháp ngữ cảnh nhạy cảm theo cách này.

+1

Không, "sử dụng công cụ đơn giản nhất" có vẻ giống như một quy tắc tốt, nhưng thực tế thì không. Ví dụ: chúng tôi sử dụng máy tính để viết thư, tuy nhiên, máy tính vào một tờ giấy giống như một cái cưa bàn so với một cái kéo. –

+0

Ý tôi là, luôn luôn có những thay đổi và nhược điểm cho mọi lựa chọn, nhưng đơn giản là một cơ sở không tốt cho một lựa chọn. * Đặc biệt * khi bạn quyết định có sử dụng Haskell hay không. : D –

+2

Có, bạn nói đúng. Nó sẽ là tốt hơn để nói một cái gì đó như, "công cụ phù hợp là một trong đó là tối đa hiệu quả trong khi được tối thiểu phức tạp." Những gì còn thiếu trong mô tả của tôi là phần về hiệu quả: bạn muốn có một công cụ đủ mạnh mẽ không chỉ để thực hiện công việc mà còn làm cho công việc dễ dàng nhất có thể. Nhưng đồng thời bạn không muốn một công cụ có nhiều chuông và còi không áp dụng được cho nhiệm vụ trong tầm tay, vì những khả năng này làm tăng độ phức tạp của việc vận hành nó lên không có lợi. –

11

Nếu trình phân tích cú pháp hoàn toàn là ứng dụng, có thể phân tích cấu trúc của nó và "tối ưu hóa" cấu trúc đó trước khi chạy. Nếu một trình phân tích cú pháp là monadic, về cơ bản nó là một chương trình Turing-complete, và thực hiện hầu như bất kỳ phân tích thú vị nào của nó tương đương với việc giải quyết vấn đề dừng (ví dụ, không thể).

Oh, và có, có sự khác biệt về phong cách quá ...

+1

Sự khác biệt giữa Áp dụng và Monad không liên quan gì đến tính năng Turing-complete. Trong Haskell, độ khó tương đối của việc tối ưu hóa các cá thể Monad chỉ là do lỗi lịch sử khi chỉ hiển thị '(>> =)' trong lớp kiểu, làm cho các trường hợp không thể cung cấp các toán tử được tối ưu hóa hơn như 'ap'. Lớp Ứng dụng tránh nhầm lẫn này và hiển thị '<*>' (tương đương với 'ap'). –

+0

@PietDelport, tôi nghĩ rắc rối là các biểu diễn cơ bản của các trình phân tích cú pháp đơn thuần thường không tuân theo các trường hợp 'Áp dụng' tối ưu, trong khi các trình phân tích cú pháp thường không hỗ trợ' >> = '. – dfeuer

+3

@PiDelport Nó hoàn toàn có tất cả mọi thứ để làm với Turing-đầy đủ. '>> =' lấy kết quả của trình phân tích cú pháp ở bên trái và chuyển nó đến bên phải, do đó làm cho trình phân tích cú pháp kết quả không thể phân tích tĩnh, bây giờ trình phân tích cú pháp tự hoàn tất. '<*>' và '<$>' không kiểm tra đối số, vì vậy trong khi đầu ra của trình phân tích cú pháp hoàn tất, vì bạn có thể đặt bất kỳ thứ gì ở bên trái của '<$>', chính khía cạnh phân tích được giới hạn và phân tích tĩnh. – semicolon

Các vấn đề liên quan