К сожалению, вы не можете сделать это вообще без хитрости на уровне типов.Тем не менее, подход в соответствии с предложением Даниэля Вагнера может быть построен в стиле DIY, используя полиморфные комбинаторы и некоторые ранее существовавшие рекурсивные примеры.Я приведу простой пример для демонстрации.
Сначала несколько простых анализаторов для тестирования:
type Parser = Parsec String ()
parseNumber :: Parser Int
parseNumber = read <$> many1 digit
parseBool :: Parser Bool
parseBool = string "yes" *> return True
<|> string "no" *> return False
parseName :: Parser String
parseName = many1 letter
Затем мы создадим тип "base case", чтобы отметить конецвозможности, и дать ему синтаксический анализатор, который всегда завершается успешно без ввода.
data Nil = Nil deriving (Eq, Ord, Read, Show, Enum, Bounded)
instance Monoid Nil where
mempty = Nil
mappend _ _ = Nil
parseNil :: Parser Nil
parseNil = return Nil
Это, конечно, эквивалентно ()
- я создаю только новый тип для устранения неоднозначности в случае, если мы действительно хотимразобрать ()
.Затем комбинатор, который объединяет синтаксические анализаторы:
infixr 3 ?|>
(?|>) :: (Monoid b) => Parser a -> Parser b -> Parser ([a], b)
p1 ?|> p2 = ((,) <$> ((:[]) <$> p1) <*> pure mempty)
<|> ((,) <$> pure [] <*> p2)
Здесь происходит то, что p1 ?|> p2
пытается p1
, и, если это удастся, переносит его в список синглтона и заполняет mempty
дляВторой элемент кортежа.Если p1
терпит неудачу, если заполняет пустой список и использует p2
для второго элемента.
parseOne :: Parser ([Int], ([Bool], ([String], Nil)))
parseOne = parseNumber ?|> parseBool ?|> parseName ?|> parseNil
Комбинирование синтаксических анализаторов с новым комбинатором является простым, а тип результата довольно понятен.
parseMulti :: Parser ([Int], ([Bool], ([String], Nil)))
parseMulti = mconcat <$> many1 (parseOne <* newline)
Мы полагаемся на рекурсивный экземпляр Monoid
для кортежей и обычный экземпляр для списков для объединения нескольких строк.Теперь, чтобы показать, что это работает, определите быстрый тест:
runTest = parseTest parseMulti testInput
testInput = unlines [ "yes", "123", "acdf", "8", "no", "qq" ]
, который успешно разбирается как Right ([123,8],([True,False],(["acdf","qq"],Nil)))
.