Как определить, использует ли 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 без ее запуска? Или еще лучше, без компиляции?

0 ответов

Другие вопросы по тегам