Группировка строк с помощью 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 ответил мне на это в Твиттере.

Как указано выше, в моем коде есть две отдельные ошибки:

  1. Парсер сам злоупотребляет <|> в то время как он должен просто последовательно анализировать строки и переходить к следующему парсеру, если он не потребляет никакого ввода.
  2. Вызов (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
Другие вопросы по тегам