Написание парсера с нуля в Haskell

Есть ли хороший учебник для написания парсера для данной грамматики в Haskell с нуля?

Я нашел:

но все они используют библиотеку parsec, и хотя это может быть интересно для промышленных приложений, я специально ищу примеры, которые "снизу вверх" без использования сложных библиотек.

Единственный, который я нашел, который использует "basic" Haskell, это: Разбор с Haskell который использует какой-то очень чуждый синтаксис (очень сложно отличить часть программы или что только "псевдокод" ), и нет четкого определения грамматики.

Любые предложения приветствуются!

Ответ 1

На самом деле удивительно легко построить Parsec-с нуля. Сам фактический код библиотеки сильно обобщен и оптимизирован, что искажает основную абстракцию, но если вы просто строите вещи с нуля, чтобы больше узнать о том, что происходит, вы можете написать его всего несколькими строками кода. Я создам чуть более слабый парсер Applicative ниже.

По существу, мы хотим создать Applicative, Parser вместе с значением примитивного анализатора

satisfy :: (Char -> Bool) -> Parser Char

и несколько комбинаторов, таких как try, который "отменяет" парсер, если он терпит неудачу

try :: Parser a -> Parser a

и orElse, что позволяет нам продолжить второй синтаксический анализатор, если первый синтаксический анализатор терпит неудачу. Обычно это записывается с помощью комбинатора infix (<|>)

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 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

Ответ 2

Мне очень понравился Грэм Хаттон "Программирование в Хаскелле". Это дает нежное введение в комбинаторы монад и парсеров. В восьмой главе построена базовая библиотека парсеров.

Вот ссылка на программирование на сайте книги Haskell. Вы также найдете ссылку на библиотеку анализатора и анализатор основных выражений.

Кроме того, если вы заинтересованы, вы также можете посмотреть uu-parsinglib компиляторы парного аппликативного стиля, разработанные в Университете Утрехта.