Как я могу сделать парсер Haskell из списка слов?

Я новичок в Haskell, использую Attoparsec для поиска цветовых выражений в тексте. Я хочу иметь возможность сопоставлять, например, «светло-синий-зеленый» и «светло-синий-зеленый» в тексте. Но, конечно, мне нужно обобщенное решение для любой такой строки. Так что я думал, что это будет что-то вроде

"light" >> sep >> "blue" >> sep >> "green"
  where sep = inClass "\n\r- "

Другими словами, я думаю, мне нужен способ вставки >> sep >> в список слов. Что-то типа:

import qualified Data.Text as T
import Data.Attoparsec.Text

-- | Makes a parser from a list of words, accepting
-- spaces, newlines, and hyphens as separators.
wordListParser :: [T.Text] -> Parser
wordListParser wordList = -- Some magic here

Или, может быть, я думаю об этом совершенно неправильно, и есть более простой способ?

Изменить: этот минимальный нерабочий пример кажется почти готовым:

{-# LANGUAGE OverloadedStrings #-}

import Replace.Attoparsec.Text
import Data.Attoparsec.Text as AT
import qualified Data.Text as T
import Control.Applicative (empty)

wordListParser :: [T.Text] -> Parser T.Text
wordListParser (w:ws) = string w >> satisfy (inClass " -") >> wordListParser ws
wordListParser [w] = string w
wordListParser [] = empty  -- or whatever the empty parser is

main :: IO ()
main = parseTest (wordListParser (T.words "light green blue")) "light green-blue"

который, я думаю, можно запустить с чем-то вроде

stack runhaskell ThisFile.hs --package attoparsec replace-attoparsec text

person Jonathan    schedule 05.02.2020    source источник
comment
Каким должен быть результат разбора light blue-green?   -  person chepner    schedule 06.02.2020
comment
Вероятно, вы захотите использовать sepBy1 и choice.   -  person chepner    schedule 06.02.2020
comment
Результат должен быть T.Text. Как бы я использовал sepBy1?   -  person Jonathan    schedule 06.02.2020
comment
Я обновил свой ответ с правкой в ​​​​вашем вопросе.   -  person MikaelF    schedule 07.02.2020


Ответы (2)


Вот что я бы сделал, предполагая, что у вас есть тип данных для ваших цветов; если вы этого не сделаете, просто замените его тем, что вы используете. Функция parseColourGen принимает любое Text, разделенное пробелом, и генерирует синтаксический анализатор, который принимает цвет, в котором каждое слово разделено одним или несколькими допустимыми разделителями.

import Prelude hiding (concat, words)
import Control.Applicative ((<|>))
import Data.Attoparsec.Text
import Data.List (intersperse)
import Data.Text (concat, pack, singleton, Text,  words)

data Colour = LightBlue | DarkBlue | VibrantRed deriving Show

parseColourGen :: Text -> Parser [Text]
parseColourGen = sequence . intersperse (mempty <$ many1 legalSep) . 
                   fmap string . words

parseColour :: [(Text, Colour)] -> Parser Colour
parseColour = foldl1 (<|>) . fmap (\(text, colour) ->
  colour <$ parseColourGen text)

legalSep :: Parser Text
legalSep = singleton <$> satisfy (inClass "\n\r- ")

Затем вы можете передать свой wordList анализатору; однако это должен быть список ассоциаций:

wordList :: [(Text, Colour)]
wordList = [("light blue", LightBlue), ("dark blue", DarkBlue), ("vibrant red", VibrantRed)]

Таким образом, вы можете настроить все свои цвета и соответствующие им названия цветов в одном месте, а затем запустить синтаксический анализатор следующим образом:

> parse (parseColour wordList) $ pack "vibrant-red"
Done "" VibrantRed

ИЗМЕНИТЬ

После редактирования вашего вопроса, я думаю, я немного лучше понимаю, чего вы хотите. FWIW, я бы все же предпочел решение выше, но вот как исправить ваш последний блок кода:

  1. Как должен сообщить вам компилятор, шаблоны (w:ws) и [w] перекрываются, поэтому, если вы хотите, чтобы среда выполнения перехватывала одноэлементный шаблон, вы должны поместить его сверху.
  2. a >> b означает "запустить действие a, отбросить его результат, затем запустить действие b и использовать этот результат". Вот почему ваш синтаксический анализатор (с приведенным выше исправлением) выведет Done "" "blue". Простой способ исправить это — использовать нотацию do для привязки результата всех трех вычислений и возврата их конкатенации.

Вот как теперь выглядит ваш код:

wordListParser :: [Text] -> Parser Text
wordListParser [w] = string w
wordListParser (w:ws) = do
  a <- string w
  b <- satisfy (inClass " -")
  c <- wordListParser ws
  return (a `append` (singleton b) `append` c) -- singleton :: Char -> Text
wordListParser [] = empty

И последнее: ваша текущая реализация не будет анализировать разрывы строк Windows (\n\r). Я не знаю, удалили ли вы \n и \r из своих символов-разделителей, но если вы этого не сделали, и файлы Windows являются для вас возможностью, об этом следует помнить.

person MikaelF    schedule 06.02.2020

Я не знаком с attoparsec, но вы могли бы использовать рекурсивное решение:

wordListParser :: [T.Text] -> Parser
wordListParser [] = empty
wordListParser [w] = text w
wordListParser (w:ws) = text w >> inClass "\n\r- " >> wordListParser ws
person bradrn    schedule 06.02.2020
comment
Есть идеи, где найти пустой парсер? Я пробовал Parser T.empty и T.empty, но не стал намного ближе. - person Jonathan; 06.02.2020
comment
@Jonathan Hoogle поймал вас. Самый первый результат — правильный. - person Daniel Wagner; 06.02.2020
comment
Это просто производит частичный разбор и не завершается. - person Jonathan; 07.02.2020
comment
@Jonathan Что вы подразумеваете под «выполняет частичный анализ»? Можете привести пример, где это не работает? - person bradrn; 07.02.2020
comment
Я только что добавил пример к своему вопросу, показывающий, где он возвращает Partial _. - person Jonathan; 07.02.2020
comment
Спасибо @Jonathan! Теперь я вижу проблему: случай [w] никогда не достигается, так как я ставлю дела в неправильном порядке. Я сейчас отредактирую свой пост. - person bradrn; 07.02.2020