Группировка строк с помощью Parsec
У меня есть строчный текстовый формат, с которым я хочу разобраться Parsec
†. Строка либо начинается со знака решетки и задает пару значений ключа, разделенных двоеточием, либо является URL-адресом, который описан предыдущими тегами.
Вот короткий пример:
#foo:bar
#faz:baz
https://example.com
#foo:beep
https://example.net
Ради простоты я собираюсь хранить все как String
, Тег - это type Tag = (String, String)
, например ("foo", "bar")
, В конечном счете, я хотел бы сгруппировать их как ([Tag], URL)
,
Тем не менее, я изо всех сил пытаюсь понять, как анализировать [один или несколько тегов] или [один URL].
Мой текущий подход выглядит так:
import qualified System.Environment as Env
import qualified Text.Megaparsec as M
import qualified Text.Megaparsec.Text as M
type Tag = (String, String)
data Segment = Tags [Tag] | URL String
deriving (Eq, Show)
tagP :: M.Parser Tag
tagP = M.char '#' *> ((,) <$> M.someTill M.printChar (M.char ':') <*> M.someTill M.printChar M.eol) M.<?> "Tag starting with #"
urlP :: M.Parser String
urlP = M.someTill M.printChar M.eol M.<?> "Some URL"
parser :: M.Parser Segment
parser = (Tags <$> M.many tagP) M.<|> (URL <$> urlP)
main :: IO ()
main = do
fname <- head <$> Env.getArgs
res <- M.parseFromFile (parser <* M.eof) fname
print res
Если я попытаюсь выполнить это на примере выше, я получу ошибку разбора, например:
3:1:
unexpected 'h'
expecting Tag starting with # or end of input
Явно мое использование many
в комбинации с <|>
это неверно. Так как анализатор тегов не будет потреблять никаких входных данных из анализатора URL, он не может быть связан с возвратом. Как мне нужно изменить это, чтобы получить желаемый результат?
Полный пример доступен на GitHub.
На самом деле я использую MegaParsec для улучшения сообщений об ошибках, но я думаю, что проблема довольно общая, а не в какой-то конкретной реализации комбинаторов синтаксического анализа.
2 ответа
@cocreature ответил мне на это в Твиттере.
Как указано выше, в моем коде есть две отдельные ошибки:
- Парсер сам злоупотребляет
<|>
в то время как он должен просто последовательно анализировать строки и переходить к следующему парсеру, если он не потребляет никакого ввода. - Вызов (
parseFromFile
) применяется толькоparser
функционировать один раз и потерпит неудачу, как только доберется до второго блока.
Мы можем исправить парсер и ввести группировку за один раз:
parser :: M.Parser ([Tag], String)
parser = liftA2 (,) (M.many tagP) urlP
После этого нам просто нужно применить изменения, предложенные leftaroundabout:
...
res <- M.parseFromFile (M.many parser <* M.eof) fname
Запуск этого приводит к желаемому результату:
[([("foo","bar"),("faz","baz")],"https://example.com"),([("foo","beep")],"https://example.net")]
То, что вы делаете, работает вполне нормально, только в данный момент вы анализируете только один сегмент (т. Е. Либо только теги, либо только URL), но это не потребляет весь ввод. Это eof
это вызывает ошибку.
Просто используйте еще один many
или же some
, чтобы учесть несколько сегментов:
main :: IO ()
main = do
fname <- head <$> Env.getArgs
res <- M.parseFromFile (many parser <* M.eof) fname
print res