Как определить, использует ли Parsec парсер постоянное пространство кучи в Haskell?
В недавнем вопросе я спросил о следующем парсере парсек:
manyLength
:: forall s u m a. ParsecT s u m a -> ParsecT s u m Int
manyLength p = go 0
where
go :: Int -> ParsecT s u m Int
go !i = (p *> go (i + 1)) <|> pure i
Эта функция похожа на many
, Однако вместо возвращения [a]
возвращает число успешных попыток p
,
Это работает хорошо, за исключением одной проблемы. Он не работает в пространстве постоянной кучи.
В связанном вопросе Ли-Яо Ся дает альтернативный способ написания manyLength
который использует пространство постоянной кучи:
manyLengthConstantHeap
:: forall s u m a. ParsecT s u m a -> ParsecT s u m Int
manyLengthConstantHeap p = go 0
where
go :: Int -> ParsecT s u m Int
go !i =
((p *> pure True) <|> pure False) >>=
\success -> if success then go (i+1) else pure i
Это значительное улучшение, но я не понимаю, почему manyLengthConstantHeap
использует постоянное пространство кучи, в то время как мой оригинал manyLength
не делает.
Если вы в строке (<|>)
в manyLength
это выглядит примерно так:
manyLengthInline
:: forall s u m a. Monad m => ParsecT s u m a -> ParsecT s u m Int
manyLengthInline p = go 0
where
go :: Int -> ParsecT s u m Int
go !i =
ParsecT $ \s cok cerr eok eerr ->
let meerr :: ParserError -> m b
meerr err =
let neok :: Int -> State s u -> ParserError -> m b
neok y s' err' = eok y s' (mergeError err err')
neerr :: ParserError -> m b
neerr err' = eerr $ mergeError err err'
in unParser (pure i) s cok cerr neok neerr
in unParser (p *> go (i + 1)) s cok cerr eok meerr
Если вы в строке (>>=)
в manyLengthConstantHeap
это выглядит примерно так:
manyLengthConstantHeapInline
:: forall s u m a. Monad m => ParsecT s u m a -> ParsecT s u m Int
manyLengthConstantHeapInline p = go 0
where
go :: Int -> ParsecT s u m Int
go !i =
ParsecT $ \s cok cerr eok eerr ->
let mcok :: Bool -> State s u -> ParserError -> m b
mcok success s' err =
let peok :: Int -> State s u -> ParserError -> m b
peok int s'' err' = cok int s'' (mergeError err err')
peerr :: ParserError -> m b
peerr err' = cerr (mergeError err err')
in unParser
(if success then go (i + 1) else pure i)
s'
cok
cerr
peok
peerr
meok :: Bool -> State s u -> ParserError -> m b
meok success s' err =
let peok :: Int -> State s u -> ParserError -> m b
peok int s'' err' = eok int s'' (mergeError err err')
peerr :: ParserError -> m b
peerr err' = eerr (mergeError err err')
in unParser
(if success then go (i + 1) else pure i)
s'
cok
pcerr
peok
peerr
in unParser ((p *> pure True) <|> pure False) s mcok cerr meok eerr
Вот конструктор 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
}
Почему manyLengthConstantHeap
работать с постоянным пространством кучи, в то время как manyLength
не? Это не похоже на рекурсивный вызов go
находится в положении хвостового вызова для любого manyLengthConstantHeap
или же manyLength
,
Когда я буду писать парсерные парсеры в будущем, как мне узнать требования к пространству для данного парсера? Как Ли-Яо Ся узнал, что manyLengthConstantHeap
было бы хорошо?
Я не чувствую уверенности в том, что могу предсказать, какие парсеры будут использовать много памяти для большого ввода.
Есть ли простой способ выяснить, будет ли данная функция хвостовой рекурсией в Haskell без ее запуска? Или еще лучше, без компиляции?