Использование кучи для CPS против не-CPS парсеров в парсеке Haskell
Я пытаюсь написать следующий парсер с использованием parsec:
manyLength
:: forall s u m a.
Monad m
=> 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]
, он возвращает количество раз Parser a
преуспевает.
Это работает, но я не могу заставить его работать в постоянном пространстве кучи. Это имеет смысл, так как рекурсивный вызов go
не находится в положении хвостового вызова.
Если parsec экспортирует конструктор в ParsecT
, можно было бы переписать manyLength
в форме CPS. Это очень похоже на manyAccum
функция:
manyLengthCPS :: forall s u m a. ParsecT s u m a -> ParsecT s u m Int
manyLengthCPS p = ParsecT f
where
f
:: forall b.
State s u
-> (Int -> State s u -> ParseError -> m b) -- consumed ok
-> (ParseError -> m b) -- consumed err
-> (Int -> State s u -> ParseError -> m b) -- empty ok
-> (ParseError -> m b) -- empty err
-> m b
f s cok cerr eok _ =
let walk :: Int -> a -> State s u -> ParseError -> m b
walk !i _ s' _ =
unParser p s'
(walk $ i + 1) -- consumed-ok
cerr -- consumed-err
manyLengthCPSErr -- empty-ok
(\e -> cok (i + 1) s' e) -- empty-err
in unParser p s (walk 0) cerr manyLengthCPSErr (\e -> eok 0 s e)
{-# INLINE f #-}
manyLengthCPSErr :: Monad m => m a
manyLengthCPSErr =
fail "manyLengthCPS can't be used on parser that accepts empty input"
это manyLengthCPS
Функция работает в постоянном пространстве кучи.
Здесь 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
}
Я тоже пытался повернуть manyLengthCPS
непосредственно в функцию без CPS с использованием низкого уровня mkPT
функция:
manyLengthLowLevel
:: forall s u m a.
Monad m
=> ParsecT s u m a -> ParsecT s u m Int
manyLengthLowLevel p = mkPT f
where
f :: State s u -> m (Consumed (m (Reply s u Int)))
f parseState = do
consumed <- runParsecT p parseState
case consumed of
Empty mReply -> do
reply <- mReply
case reply of
Ok _ _ _ -> manyLengthErr
Error parseErr -> pure . Empty . pure $ Ok 0 parseState parseErr
Consumed mReply -> do
reply <- mReply
case reply of
Ok a newState parseErr -> walk 0 a newState parseErr
Error parseErr -> pure . Consumed . pure $ Error parseErr
where
walk
:: Int
-> a
-> State s u
-> ParseError
-> m (Consumed (m (Reply s u Int)))
walk !i _ parseState' _ = do
consumed <- runParsecT p parseState'
case consumed of
Empty mReply -> do
reply <- mReply
case reply of
Ok _ _ _ -> manyLengthErr
Error parseErr ->
pure . Consumed . pure $ Ok (i + 1) parseState' parseErr
Consumed mReply -> do
reply <- mReply
case reply of
Ok a newState parseErr -> walk (i + 1) a newState parseErr
Error parseErr -> pure . Consumed . pure $ Error parseErr
manyLengthErr :: Monad m => m a
manyLengthErr =
fail "manyLengthLowLevel can't be used on parser that accepts empty input"
Как manyLength
, manyLengthLowLevel
не работает в пространстве постоянной кучи.
Можно ли написать manyLength
так он работает в пространстве постоянной кучи, даже не записав его в стиле CPS? Если нет, то почему нет? Есть ли фундаментальная причина, по которой это возможно в стиле CPS, но не в стиле не CPS?
1 ответ
Это работает в постоянном пространстве кучи. Идея состоит в том, чтобы сначала попробовать p
и в явном виде выполнить анализ кейса по результату его успеха, чтобы решить, стоит ли go
или нет, так что go
заканчивается в хвостовой позиции вызова.
manyLength
:: Monad m
=> ParsecT s u m a -> ParsecT s u m Int
manyLength p = go 0
where
go :: Int -> ParsecT s u m Int
go !i = do
success <- (p *> pure True) <|> pure False
if success then go (i+1) else pure i