В чем разница между функциями "try" и "lookAhead" в parsec?
Parsec: разница между "try" и "lookAhead"?
Ответ 1
Комбинаторы try
и lookAhead
аналогичны, поскольку они оба позволяют Parsec "перематывать", но они применяются в разных обстоятельствах. В частности, try
перематывает сбой, а lookAhead
перематывает успех.
В документации try
"притворяется, что он не потреблял никаких входных данных при возникновении ошибки, тогда как lookAhead p
" анализирует p
, не потребляя никакого ввода ", но" if p
терпит неудачу и потребляет некоторые input, то есть lookAhead
".
Итак, если вы думаете о том, что парсер работает как ходьба по некоторому потоковому состоянию и либо не работает, либо преуспевает, что мы можем записать в терминах Haskell как
type Parser a = [Tokens] -> (Either Error a, [Tokens])
то try
гарантирует, что если (try p) input ---> (Left err, output)
, то input == output
и lookAhead
имеет такое значение, что (lookAhead p) input ---> (Right a, output)
then input == output
, но если (lookAhead p) input ---> (Left err, output)
, то им может быть разрешено различать.
Мы можем увидеть это в действии, посмотрев на код для Parsec напрямую, что несколько сложнее, чем мое понятие Parser
выше. Сначала рассмотрим ParsecT
newtype ParsecT s u m a
= ParsecT {unParser :: forall b .
State s u
-> (a -> State s u -> ParseError -> m b) -- consumed ok
-> (ParseError -> m b) -- consumed err
-> (a -> State s u -> ParseError -> m b) -- empty ok
-> (ParseError -> m b) -- empty err
-> m b
}
ParsecT
- это тип данных, основанный на продолжении. Если вы посмотрите, как один из них построен
ParsecT $ \s cok cerr eok eerr -> ...
Вы увидите, как у нас есть доступ к State s u
, s
и четырем функциям, которые определяют, как мы продвигаемся вперед. Например, предложение fail
экземпляра ParsecT
Monad
использует параметр eerr
, создавая ParseError
из текущей позиции ввода и переданного сообщения об ошибке.
parserFail :: String -> ParsecT s u m a
parserFail msg
= ParsecT $ \s _ _ _ eerr ->
eerr $ newErrorMessage (Message msg) (statePos s)
В то время как самый примитивный успешный анализ токена (tokenPrim
) использует сложную последовательность событий, в конечном итоге кульминацию которой вызывает вызов cok
с обновленным State s u
.
С этой интуицией источник try
особенно прост.
try :: ParsecT s u m a -> ParsecT s u m a
try p =
ParsecT $ \s cok _ eok eerr ->
unParser p s cok eerr eok eerr
Он просто создает новый ParsecT
на основе переданного для попытки, но с продолжением "empty err"
вместо потребленной ошибки. Вне зависимости от того, компилятор синтаксического анализа видит, что try p
не сможет получить доступ к его фактическому продолжению "consumed err"
, и, таким образом, попытка защищена от изменения его состояния при ошибках.
Но lookAhead
более сложный
lookAhead :: (Stream s m t) => ParsecT s u m a -> ParsecT s u m a
lookAhead p = do{ state <- getParserState
; x <- p'
; setParserState state
; return x
}
where
p' = ParsecT $ \s cok cerr eok eerr ->
unParser p s eok cerr eok eerr
Изучая только where
-clause, мы видим, что это зависит от модификации прошедшего парсера p
, чтобы использовать продолжение "empty ok"
вместо продолжения "consumed ok"
. Это симметрично тому, что сделал try
. Кроме того, он гарантирует, что состояние парсера не зависит от того, что происходит, когда этот измененный p'
запускается через его do
-block.