Сопоставление подмножеств скобок с символами

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

val s = "((2((x+3)+6)))"
val map = mapParentheses(s)

Где s может содержать любое количество наборов скобок, а возвращаемая карта должна содержать:

"(x+3)" -> 'a'

"(a+6)" -> 'b'

"(2b)" -> 'c'

"(c)" -> 'd'

Так что в другом месте моей программы я могу вызвать 'd' и получить "(c)", который станет "((2b))" затем ((2(a+6))) и, наконец, ((2((x+3)+6))). Строка, отправляемая методу mapParentheses, никогда не будет содержать несовпадающих круглых скобок или дополнительных символов за пределами основных родительских круглых скобок, поэтому следующие элементы никогда не будут отправлены:

  • "(fsf)a", поскольку a находится за пределами родительских скобок
  • "(a(aa))(a)", поскольку (a) находится за пределами родительских скобок
  • "((a)", поскольку скобки не совпадают
  • ")a(", потому что скобки не совпадают

Поэтому мне было интересно, знает ли кто-нибудь о простом (или не простом) способе создания этого метода mapParentheses.


person Seren    schedule 15.09.2012    source источник
comment
Можете ли вы иметь несколько скобок на одном уровне (например, ((x + 1) + (y + 2)))?   -  person Travis Brown    schedule 16.09.2012
comment
@TravisBrown - не имеет значения. Строки гарантированно верны, и общее решение так же просто, как и предполагающее, что на одном уровне не будет нескольких блоков.   -  person Rex Kerr    schedule 16.09.2012
comment
@RexKerr: вы можете написать немного более красивую версию комбинатора синтаксического анализатора, если знаете, что у вас есть только один на уровень.   -  person Travis Brown    schedule 16.09.2012
comment
@TravisBrown - Хорошо, вы продемонстрировали это, я согласен.   -  person Rex Kerr    schedule 16.09.2012
comment
@TravisBrown - Да, возможны любые комбинации скобок, но вся строка будет заключена в родительский набор скобок.   -  person Seren    schedule 16.09.2012


Ответы (3)


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

trait Part {
  def text: String
  override def toString = text
}
class Text(val text: String) extends Part {}
class Parens(val contents: Seq[Part]) extends Part {
  val text = "(" + contents.mkString + ")"
  def mapText(m: Map[Parens, Char]) = {
    val inside = contents.collect{
      case p: Parens => m(p).toString
      case x => x.toString
    }
    "(" + inside.mkString + ")"
  }
  override def equals(a: Any) = a match {
    case p: Parens => text == p.text
    case _ => false
  }
  override def hashCode = text.hashCode
}

Теперь вам нужно разобрать на эти вещи:

def str2parens(s: String): (Parens, String) = {
  def fail = throw new Exception("Wait, you told me the input would be perfect.")
  if (s(0) != '(') fail
  def parts(s: String, found: Seq[Part] = Vector.empty): (Seq[Part], String) = {
    if (s(0)==')') (found,s)
    else if (s(0)=='(') {
      val (p,s2) = str2parens(s)
      parts(s2, found :+ p)
    }
    else {
      val (tx,s2) = s.span(c => c != '(' && c != ')')
      parts(s2, found :+ new Text(tx))
    }
  }
  val (inside, more) = parts(s.tail)
  if (more(0)!=')') fail
  (new Parens(inside), more.tail)
}

Теперь мы разобрали все это. Итак, давайте найдем все биты.

def findParens(p: Parens): Set[Parens] = {
  val inside = p.contents.collect{ case q: Parens => findParens(q) }
  inside.foldLeft(Set(p)){_ | _}
}

Теперь мы можем построить карту, которую вы хотите.

def mapParentheses(s: String) = {
  val (p,_) = str2parens(s)
  val pmap = findParens(p).toSeq.sortBy(_.text.length).zipWithIndex.toMap
  val p2c = pmap.mapValues(i => ('a'+i).toChar)
  p2c.map{ case(p,c) => (p.mapText(p2c), c) }.toMap
}

Доказательства того, что это работает:

scala> val s = "((2((x+3)+6)))"
s: java.lang.String = ((2((x+3)+6)))

scala> val map = mapParentheses(s)
map: scala.collection.immutable.Map[java.lang.String,Char] =
  Map((x+3) -> a, (a+6) -> b, (2b) -> c, (c) -> d)

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

person Rex Kerr    schedule 15.09.2012
comment
Зачем страдать от всего этого if (x(0)=='(')-ing, когда у вас есть комбинаторы парсеров в стандартной библиотеке? - person Travis Brown; 16.09.2012
comment
@TravisBrown - потому что когнитивное бремя добавления библиотеки синтаксического анализа, когда у вас есть почти тривиальная задача, не стоит того, если вы хотите понять, как на самом деле работает решение (а не что оно работает). - person Rex Kerr; 16.09.2012
comment
Справедливо, хотя я думаю, что подход комбинатора синтаксического анализатора на самом деле делает структуру проблемы намного яснее, даже в таком довольно простом случае, как этот. - person Travis Brown; 16.09.2012
comment
@TravisBrown - Это, безусловно, проясняет некоторые детали, но есть немало волшебства, которое нужно понять. Как только вы поймете как поверхностный уровень, так и внутреннюю магию, я согласен, что это лучший способ выразить логическую структуру проблемы. - person Rex Kerr; 16.09.2012

Вы можете сделать это довольно легко с помощью комбинаторов синтаксического анализатора Scala. Сначала об импорте и некоторых простых структурах данных:

import scala.collection.mutable.Queue
import scala.util.parsing.combinator._

sealed trait Block {
  def text: String
}

case class Stuff(text: String) extends Block

case class Paren(m: List[(String, Char)]) extends Block {
  val text = m.head._2.toString
  def toMap = m.map { case (k, v) => "(" + k + ")" -> v }.toMap
}

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

Теперь о самом парсере:

class ParenParser(fresh: Queue[Char]) extends RegexParsers {
  val stuff: Parser[Stuff] = "[^\\(\\)]+".r ^^ (Stuff(_))

  def paren: Parser[Paren] = ("(" ~> insides <~ ")") ^^ {
    case (s, m) => Paren((s -> fresh.dequeue) :: m)
  }

  def insides: Parser[(String, List[(String, Char)])] =
    rep1(paren | stuff) ^^ { blocks =>
      val s = blocks.flatMap(_.text)(collection.breakOut)
      val m = blocks.collect {
        case Paren(n) => n
      }.foldLeft(List.empty[(String, Char)])(_ ++ _)
      (s, m)
    }

  def parse(input: String) = this.parseAll(paren, input).get.toMap
}

Использование get в последней строке далеко не идеально, но оправдано вашим утверждением о том, что мы можем ожидать корректного ввода.

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

val parser = new ParenParser(Queue('a', 'b', 'c', 'd', 'e', 'f'))

А теперь попробуйте свою тестовую строку:

scala> println(parser parse "((2((x+3)+6)))")
Map((c) -> d, (2b) -> c, (a+6) -> b, (x+3) -> a)

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

person Travis Brown    schedule 15.09.2012
comment
Не лучше ли взять Iterator[Char] (или => Char), чтобы при желании можно было сгенерировать бесконечный источник? - person Rex Kerr; 16.09.2012

def parse(s: String, 
  c: Char = 'a', out: Map[Char, String] = Map() ): Option[Map[Char, String]] =
  """\([^\(\)]*\)""".r.findFirstIn(s) match {
    case Some(m) => parse(s.replace(m, c.toString), (c + 1).toChar , out + (c -> m))
    case None if s.length == 1 => Some(out)
    case _ => None
  }

Это выводит Option, содержащее Map, если он анализируется, что лучше, чем выдавать исключение, если это не так. Я подозреваю, что вы действительно хотели получить карту от Char до String, вот что это выводит. c и out являются параметрами по умолчанию, поэтому вам не нужно вводить их самостоятельно. Регулярное выражение просто означает «любое количество символов, которые не являются скобками, заключенными в скобки» (символы скобок должны быть экранированы с помощью «\»). findFirstIn находит первое совпадение и возвращает Option[String], для которого мы можем найти совпадение с шаблоном, заменив эту строку соответствующим символом.

val s = "((2((x+3)+6)))"
parse(s)  //Some(Map(a -> (x+3), b -> (a+6), c -> (2b), d -> (c)))
parse("(a(aa))(a)") //None
person Luigi Plinge    schedule 17.09.2012