丹尼爾菲捨爾提出了一個具體的修復;我想建議一個抽象的修復。一路上,我們會公開一個您在此做出的設計決定,您甚至可能沒有意識到自己已經做出了決定(並且我會做出錯誤決定的情況)。
是應用函子組成的well known;它雖然不是我自己發明的,但其他種類的函子配對組合起來卻不是那麼有名。特別是,一個替代函子與任一側的函子組成一個新的替代函數。下面,我將使用Haskell語法來解釋我的意思。我會寫無效 Haskell - 因爲我將在任何地方使用type
而不是newtype
以避免混亂 - 但我們會在以後使用無效的Haskell派生有效的Haskell。
type (f :. g) a = f (g a) -- like in TypeCompose
-- (1)
instance (Applicative f, Alternative g) => Alternative (f :. g) where
empty = pure empty
x <|> y = liftA2 (<|>) x y
-- (2)
instance Alternative f => Alternative (f :. g) where
empty = empty
x <|> y = x <|> y
(這些情況下,重疊的真的,真的很多。)
語義上說,我們現在可以查看您的類型爲類型組成的鏈:
-- (3)
type Parser = (String ->) :. [] :. (String,)
...其中我們在這裏觀察(String ->)
是Applicative
(4)的一個實例,並且[]
是Alternative
(5)的實例。這意味着我們應該能夠通過將Parser
的語義定義與上面的實例耦合,從此「讀取」Alternative
的實例。
empty :: Parser t
= -- (3)
empty :: (String ->) :. [] :. (String,)
= -- (1)
pure (empty :: [] :. (String,)) :: (String ->) :. ([] :. (String,))
= -- (4)
const (empty :: [] :. (String,)) :: (String ->) :. ([] :. (String,))
= -- (2) to use []'s empty rather than [] :. (String,)'s empty
const (empty :: [] :. (String,)) :: (String ->) :. ([] :. (String,))
= -- (5)
const [] :: (String ->) :. ([] :. (String,))
p <|> q :: Parser t -> Parser t -> Parser t
= -- (3)
p <|> q :: ... -> ... -> ((String ->) :. [] :. (String,))
= -- (1)
liftA2 (<|>) p q
= -- (4)
\s -> p s <|> q s
= -- (2) to use []'s <|> rather than [] :. (String,)'s <|>
\s -> p s <|> q s
= -- (5)
\s -> p s ++ q s
所以,語義,我們知道我們要怎麼empty
和<|>
現在表現爲Parser
s,而剩下的唯一的訣竅就是在所有適當的NEWTYPE建設者和deconstructors增加。
instance Alternative Parser where
empty = Parser (const [])
Parser p <|> Parser q = Parser (\s -> p s ++ q s)
或者,如果我們覺得興奮,我們可以寫同樣的事情更重載語法:
instance Alternative Parser where
empty = Parser (pure empty)
Parser p <|> Parser q = Parser (liftA2 (<|>) p q)
注意,這個實現的(<|>)
實際上總是返回從q
所有的結果!在你的定義中,當p
失敗時,q
只返回其解析;這尤其意味着成功的解析列表將會左偏。上面的語義驅動實現沒有這樣的偏見:即使(<|>)
的左側解析,也可以允許右側講述它的勝利。我認爲這很自然:這意味着使用此接口構建的解析器將返回所有成功的解析。
實際上有什麼區別?那麼上面的語義定義是更穩健,並且您提出的定義是效率更高。讓我們先看看「更健壯」的含義。
考慮一個解析器總是消耗只有一個字符(它返回什麼將是不重要的討論):
oneChar = Parser (\s -> case s of
c:cs -> [((),cs)]
_ -> [])
...並總是成功不消耗任何字符的解析器:
epsilon = Parser (\s -> [((),s)]) -- you might recognize this as "pure()"
現在,如果我們編寫這樣的一個或兩個字符的解析器會發生什麼?
oneOrTwo = (oneChar <|> epsilon) <* oneChar
考慮使用oneOrTwo
解析"a"
。在你的定義爲(<|>)
時,第一部分oneChar <|> epsilon
嘗試使用oneChar
解析一個成功的位,因此永遠不會運行epsilon
,我們將得到一個解析列表,如[((),"")]
。但是現在第二部分失敗了:沒有一個字符需要解析。根據我對(<|>)
的定義,第一部分將嘗試同時運行oneChar
和epsilon
的兩個解析,並獲得如[((),""),((),"a")]
的解析列表。現在,第二部分在列表的第一個元素上失敗,但在第二個元素上成功,並且解析總體成功。另一方面,由於兩個原因,你的定義可以更有效:首先,它可以儘早丟棄輸入的早期部分(導致與垃圾收集更好的交互),其次,它可以修剪大部分的回溯搜索空間(導致搜索週期更少)。
這種權衡是相當知名的;例如,Parsec通過try組合子提供了你的交替類型(它向第一個參數提交,如果它消耗任何輸入的話)和我的類型(它會回溯)。