Написание парсера с нуля на Хаскеле
Есть ли хороший учебник для написания парсера для данной грамматики в Haskell с нуля?
Я нашел:
но все они используют библиотеку parsec, и хотя это может быть интересно для промышленных приложений, я специально ищу примеры "снизу вверх" без использования сложных библиотек.
Единственный, который я нашел, который использует "базовый" Haskell, это: синтаксический анализ с Haskell, который использует какой-то очень посторонний синтаксис (очень трудно различить, что является частью программы или что является только "псевдокодом"), и нет четкого определения грамматики.
Любые предложения высоко ценятся!
2 ответа
На самом деле удивительно легко создать Parsec с нуля. Сам код библиотеки в значительной степени обобщен и оптимизирован, что искажает основную абстракцию, но если вы просто строите вещи с нуля, чтобы лучше понять, что происходит, вы можете написать это всего за несколько строк кода. Я построю немного слабее Applicative
парсер ниже.
По сути, мы хотим произвести Applicative
, Parser
наряду с примитивным значением парсера
satisfy :: (Char -> Bool) -> Parser Char
и несколько комбинаторов, таких как try
, который "отменяет" синтаксический анализатор, если он терпит неудачу
try :: Parser a -> Parser a
а также orElse
что позволяет нам продолжить работу со вторым парсером, если первый парсер не работает. Обычно это написано с помощью комбинатора инфикса. (<|>)
orElse, (<|>) :: Parser a -> Parser a -> Parser a
Так как наш Applicative
необходимо отслеживать текущее состояние потока и иметь возможность сбоя, мы создадим его путем объединения состояния Applicative
и Either
аппликативны.
type Error = String
newtype Parser a = P { unP :: String -> (String, Either Error a) }
instance Functor Parser where
fmap f (P st) = P $ \stream -> case st stream of
(res, Left err) -> (res, Left err)
(res, Right a ) -> (res, Right (f a))
instance Applicative Parser where
pure a = P (\stream -> (stream, Right a))
P ff <*> P xx = P $ \stream0 -> case ff stream0 of -- produce an f
(stream1, Left err) -> (stream1, Left err)
(stream1, Right f ) -> case xx stream1 of -- produce an x
(stream2, Left err) -> (stream2, Left err)
(stream2, Right x ) -> (stream2, Right (f x)) -- return (f x)
Если мы будем следовать (<*>)
метод в Applicative
экземпляр тщательно мы видим, что он просто пропускает поток в f
-продуцирующих Parser
, принимает поток результатов и, в случае успеха, передает его x
-продуцирующих Parser
и если они оба преуспевают, это возвращает их заявление (f x)
, Это означает, что если у нас есть парсер, создающий функцию, и парсер, генерирующий аргументы, мы можем упорядочить их с (<*>)
-- given
parseChar :: Char -> Parser Char
parseHi :: Parser (Char, Char) -- parses 'h' then 'i'
parseHi = pure (,) <$> parseChar 'h' <*> parseChar 'i'
Мы можем использовать механику этого Applicative
построить необходимые комбинаторы, а также. Вот satisfy
-- | Peek at the next character and return successfully if it satisfies a predicate
satisfy :: (Char -> Bool) -> Parser Char
satisfy f = P $ \stream -> case stream of
[] -> ([], Left "end of stream")
(c:cs) | f c -> (cs, Right c)
| otherwise -> (cs, Left "did not satisfy")
И вот try
-- | Run a parser but if it fails revert the stream to it's original state
try :: Parser a -> Parser a
try (P f) = P $ \stream0 -> case f stream0 of
(_ , Left err) -> (stream0, Left err)
(stream1, Right a ) -> (stream1, Right a )
И вот orElse
orElse :: Parser a -> Parser a -> Parser a
orElse (P f1) (P f2) = P $ \stream0 -> case f1 stream0 of
(stream1, Left err) -> f2 stream1
(stream1, Right a ) -> (stream1, Right a)
Обычно в этот момент мы также отмечаем, что Parser
образует Alternative
экземпляр с orElse
если мы также предоставим парсер с немедленным выходом из строя, empty
instance Alternative Parser where
empty = P $ \stream -> (stream, Left "empty")
(<|>) = orElse
many = manyParser
some = someParser
И мы можем написать manyParser
а также someParser
как комбинаторы, которые запускают парсер неоднократно.
-- | 0 or more
manyParser :: Parser a -> Parser [a]
manyParser (P f) = P go where
go stream = case f stream of
(_ , Left err) -> (stream, Right []) -- throws away the error
(stream', Right a ) -> case go stream' of
(streamFin, Left err) -> (streamFin, Left err)
(streamFin, Right as) -> (streamFin, Right (a : as))
-- | 1 or more
someParser :: Parser a -> Parser [a]
someParser (P f) = P $ \stream -> case f stream of
(stream', Left err) -> (stream', Left err)
(stream', Right a ) ->
let (P fmany) = manyParser (P f)
in case fmany stream' of
(stream'', Left err) -> (stream'', Left err)
(stream'', Right as) -> (stream'', Right (a:as))
И отсюда мы можем начать работать на более высоких уровнях абстракции.
char :: Char -> Parser Char
char c = satisfy (== c)
string :: String -> Parser String
string [] = pure []
string (c:cs) = (:) <$> char c <*> string cs
oneOf :: [Char] -> Parser Char
oneOf cs = satisfy (`elem` cs)
parens :: Parser a -> Parser a
parens parseA = dropFirstAndLast <$> char '(' <*> parseA <*> char ')'
where
dropFirstAndLast _ a _ = a
Мне очень понравилась "Программирование в Хаскеле" Грэма Хаттона. Это дает нежное введение в монады и парсер комбинаторы. Восьмая глава создает базовую библиотеку синтаксического анализатора.
Вот ссылка на Программирование на сайте книги Haskell. Вы также найдете ссылку на библиотеку парсера и парсер базовых выражений.
Кроме того, если вам интересно, вы также можете посмотреть на комбинаторы парсера аппликативного стиля uu-parsinglib, разработанные в Утрехтском университете.