Передача дополнительного состояния через синтаксический анализатор в Scala

Я дам вам tl; dr вперед

Я пытаюсь использовать преобразователь монад состояния в Scalaz 7 для передачи дополнительного состояния через синтаксический анализатор и У меня проблемы с выполнением чего-либо полезного, не написав много t m a -> t m b версий m a -> m b методов.

Пример проблемы парсинга

Предположим, у меня есть строка, содержащая вложенные круглые скобки с цифрами внутри них:

val input = "((617)((0)(32)))"

У меня также есть поток свежих имен переменных (в данном случае символов):

val names = Stream('a' to 'z': _*)

Я хочу вытащить имя из верхней части потока и назначить его каждому выражению в скобках по мере его анализа, а затем сопоставить это имя со строкой, представляющей содержимое круглых скобок, с замененными вложенными выражениями в скобках (если есть) на их имена.

Чтобы сделать это более конкретным, вот как я бы хотел, чтобы результат выглядел для примера ввода выше:

val target = Map(
  'a' -> "617",
  'b' -> "0",
  'c' -> "32",
  'd' -> "bc",
  'e' -> "ad"
)

На данном уровне может быть либо строка цифр, либо произвольно много подвыражений, но эти два вида содержимого не будут смешаны в одном выражении в скобках.

Для простоты предположим, что поток имен никогда не будет содержать ни дубликатов, ни цифр, и что он всегда будет содержать достаточно имен для нашего ввода.

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

Приведенный выше пример представляет собой немного упрощенную версию проблемы синтаксического анализа в этом вопросе о переполнении стека. Я ответил на этот вопрос решением, которое выглядело примерно так:

import scala.util.parsing.combinator._

class ParenParser(names: Iterator[Char]) extends RegexParsers {
  def paren: Parser[List[(Char, String)]] = "(" ~> contents <~ ")" ^^ {
    case (s, m) => (names.next -> s) :: m
  }

  def contents: Parser[(String, List[(Char, String)])] = 
    "\\d+".r ^^ (_ -> Nil) | rep1(paren) ^^ (
      ps => ps.map(_.head._1).mkString -> ps.flatten
    )

  def parse(s: String) = parseAll(paren, s).map(_.toMap)
}

Это не так уж и плохо, но я бы предпочел избегать изменчивого состояния.

Что я хочу

Библиотека Haskell Parsec упрощает добавление состояния пользователя в парсер:

import Control.Applicative ((*>), (<$>), (<*))
import Data.Map (fromList)
import Text.Parsec

paren = do
  (s, m) <- char '(' *> contents <* char ')'
  h : t  <- getState
  putState t
  return $ (h, s) : m
  where
    contents
      =  flip (,) []
     <$> many1 digit
     <|> (\ps -> (map (fst . head) ps, concat ps))
     <$> many1 paren

main = print $
  runParser (fromList <$> paren) ['a'..'z'] "example" "((617)((0)(32)))"

Это довольно простой перевод моего парсера Scala выше, но без изменяемого состояния.

Что я пробовал

Я пытаюсь максимально приблизиться к решению Parsec, используя преобразователь монад состояний Scalaz, поэтому вместо Parser[A] я работаю с StateT[Parser, Stream[Char], A]. У меня есть "решение", которое позволяет мне написать следующее:

import scala.util.parsing.combinator._
import scalaz._, Scalaz._

object ParenParser extends ExtraStateParsers[Stream[Char]] with RegexParsers {
  protected implicit def monadInstance = parserMonad(this)

  def paren: ESP[List[(Char, String)]] = 
    (lift("(" ) ~> contents <~ lift(")")).flatMap {
      case (s, m) => get.flatMap(
        names => put(names.tail).map(_ => (names.head -> s) :: m)
      )
    }

  def contents: ESP[(String, List[(Char, String)])] =
    lift("\\d+".r ^^ (_ -> Nil)) | rep1(paren).map(
      ps => ps.map(_.head._1).mkString -> ps.flatten
    )

  def parse(s: String, names: Stream[Char]) =
    parseAll(paren.eval(names), s).map(_.toMap)
}

Это работает, и это не намного менее кратко, чем версия с изменяемым состоянием или версия Parsec.

Но мой ExtraStateParsers уродлив, как грех - я не хочу испытывать ваше терпение больше, чем у меня уже есть, поэтому я не буду включать его здесь (хотя вот ссылка, если вы действительно этого хотите). Мне пришлось написать новые версии каждого Parser и Parsers метода, который я использовал выше для моих типов ExtraStateParsers и ESP (rep1, ~>, <~ и |, если вы считаете). Если бы мне пришлось использовать другие комбинаторы, мне пришлось бы также написать новые их версии на уровне преобразователя состояний.

Есть ли более чистый способ сделать это? Мне бы хотелось увидеть пример преобразователя монад состояния Scalaz 7, который используется для потоковой передачи состояния через синтаксический анализатор, но примеры Scalaz 6 или Haskell также были бы полезны и признательны.


person Travis Brown    schedule 19.09.2012    source источник
comment
Я считаю, что решение Iterator с изменяемым состоянием является наиболее идиоматическим решением Scala.   -  person Dan Burton    schedule 19.09.2012
comment
@DanBurton: Я согласен, но избегание такого рода изменяемого состояния может быть интересным (а иногда и полезным) упражнением, и в этом случае есть явно применимый неизменяемый подход - я просто не могу понять, как его использовать чисто.   -  person Travis Brown    schedule 19.09.2012
comment
Это выполнимо, если вы реализуете Parser как преобразователь монад (с именем ParserT?), А затем реализуете экземпляр MonadState для ParserT, если его внутренняя монада является экземпляром MonadState.   -  person Yo Eight    schedule 19.09.2012
comment
@YoEight: Я думал об этом подходе, но он казался более сложным, поскольку я использую стандартную библиотеку Parser. Я все же разберусь - спасибо.   -  person Travis Brown    schedule 19.09.2012


Ответы (1)


Вероятно, наиболее общим решением было бы переписать библиотеку синтаксического анализатора Scala для размещения монадических вычислений во время синтаксического анализа (как вы частично это сделали), но это будет довольно трудоемкой задачей.

Я предлагаю решение, используя ScalaZ. /scalaz/scalaz-2.9.1-6.0.4/doc/index.html#scalaz.State "rel =" noreferrer "> Состояние, где каждый наш результат не является значением типа Parse[X], а значение типа Parse[State[Stream[Char],X]] (псевдоним ParserS[X]). Таким образом, общий анализируемый результат - это не значение, а монадическое значение состояния, которое затем запускается на некотором Stream[Char]. Это почти монадный преобразователь, но подъем / разгрузку приходится делать вручную. Это делает код немного уродливее, так как нам иногда нужно поднимать значения или использовать _5 _ / _ 6_ в нескольких местах, но я считаю, что это все еще разумно.

import scala.util.parsing.combinator._
import scalaz._
import Scalaz._
import Traverse._

object ParenParser extends RegexParsers with States {
  type S[X] = State[Stream[Char],X];
  type ParserS[X] = Parser[S[X]];


  // Haskell's `return` for States
  def toState[S,X](x: X): State[S,X] = gets(_ => x)

  // Haskell's `mapM` for State
  def mapM[S,X](l: List[State[S,X]]): State[S,List[X]] =
    l.traverse[({type L[Y] = State[S,Y]})#L,X](identity _);

  // .................................................

  // Read the next character from the stream inside the state
  // and update the state to the stream's tail.
  def next: S[Char] = state(s => (s.tail, s.head));


  def paren: ParserS[List[(Char, String)]] =
    "(" ~> contents <~ ")" ^^ (_ flatMap {
      case (s, m) => next map (v => (v -> s) :: m)
    })


  def contents: ParserS[(String, List[(Char, String)])] = digits | parens;
  def digits: ParserS[(String, List[(Char, String)])] =
    "\\d+".r ^^ (_ -> Nil) ^^ (toState _)
  def parens: ParserS[(String, List[(Char, String)])] =
    rep1(paren) ^^ (mapM _) ^^ (_.map(
        ps => ps.map(_.head._1).mkString -> ps.flatten
      ))


  def parse(s: String): ParseResult[S[Map[Char,String]]] =
    parseAll(paren, s).map(_.map(_.toMap))

  def parse(s: String, names: Stream[Char]): ParseResult[Map[Char,String]] =
    parse(s).map(_ ! names);
}

object ParenParserTest extends App {
  {
    println(ParenParser.parse("((617)((0)(32)))", Stream('a' to 'z': _*)));
  }
}

Примечание. Я считаю, что ваш подход к StateT[Parser, Stream[Char], _] концептуально неверен. Тип говорит, что мы создаем синтаксический анализатор с учетом некоторого состояния (потока имен). Таким образом, возможно, что при разных потоках мы получим разные парсеры. Это не то, что мы хотим делать. Нам нужно только, чтобы результат синтаксического анализа зависел от имен, а не от всего синтаксического анализатора. Таким образом, Parser[State[Stream[Char],_]] кажется более подходящим (Parsec в Haskell использует аналогичный подход, состояние / монада находится внутри парсера).

person Petr    schedule 22.09.2012
comment
Спасибо! Это очень полезно. По поводу вашего последнего замечания - в моей первоначальной формулировке проблемы я хотел, чтобы парсер отказывал, если в потоке заканчивались имена, что и определяло форму моего стека. Я попробую твой подход - еще раз спасибо. - person Travis Brown; 22.09.2012