Подтвердить что ты не робот

Замените значения в строке с помощью заполнителей в Scala

Я только начал использовать Scala и хотел бы лучше понять функциональный подход к решению проблем. У меня есть пары строк, у первых есть заполнители для параметра, и у пары есть значения, которые нужно заменить. например "выберите col1 из tab1, где id > $1 и имя типа $2" "параметры: $1 = '250', $2 = 'some%'"

Может быть много более двух параметров.

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

Может ли кто-нибудь указать мне на функциональный подход, который будет более аккуратным и менее подверженным ошибкам?

4b9b3361

Ответ 1

Говоря строго о проблеме замены, моим предпочтительным решением является функция, которая может быть доступна в предстоящем Scala 2.8, что позволяет заменять шаблоны регулярных выражений с помощью функции. Используя его, проблему можно свести к следующему:

def replaceRegex(input: String, values: IndexedSeq[String]) =  
  """\$(\d+)""".r.replaceAllMatchesIn(input, {
    case Regex.Groups(index) => values(index.toInt)
  })

Что уменьшает проблему до того, что вы на самом деле собираетесь делать: замените все шаблоны $N на соответствующее N-е значение списка.

Или, если вы действительно можете установить стандарты для своей строки ввода, вы можете сделать это следующим образом:

"select col1 from tab1 where id > %1$s and name like %2$s" format ("one", "two")

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

Мышление функционально об этом означает мышление о функции. У вас есть строка, некоторые значения, и вы хотите вернуть строку. В статически типизированном функциональном языке это означает, что вы хотите что-то вроде этого:

(String, List[String]) => String

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

(String, IndexedSeq[String]) => String

Это должно быть достаточно хорошо для нашей функции. Теперь, как мы разрушаем работу? Есть несколько стандартных способов сделать это: рекурсия, понимание, сгибание.

RECURSION

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

  • Заменить первый заполнитель
  • Повторите с оставшимися заполнителями

Это на самом деле довольно прямолинейно, поэтому давайте вдав в подробности. Как заменить первый заполнитель? Единственное, чего нельзя избежать, это то, что мне нужно знать, что это заполнитель, потому что мне нужно получить индекс от моих значений. Поэтому мне нужно найти его:

(String, Pattern) => String

После того как я найден, я могу заменить его на строку и повторить:

val stringPattern = "\\$(\\d+)"
val regexPattern = stringPattern.r
def replaceRecursive(input: String, values: IndexedSeq[String]): String = regexPattern findFirstIn input match {
  case regexPattern(index) => replaceRecursive(input replaceFirst (stringPattern, values(index.toInt)))
  case _ => input // no placeholder found, finished
}

Это неэффективно, потому что он многократно производит новые строки, а не просто конкатенирует каждую часть. Попытайтесь быть более умными в этом.

Чтобы эффективно построить строку с помощью конкатенации, нам нужно использовать StringBuilder. Мы также хотим избежать создания новых строк. StringBuilder может принимать CharSequence, который мы можем получить из String. Я не уверен, что новая строка действительно создана или нет - если это так, мы могли бы свернуть собственный CharSequence таким образом, чтобы он отображался как String вместо создания нового String. Заверили, что мы сможем легко изменить это, если потребуется, я буду исходить из предположения, что это не так.

Итак, рассмотрим, какие функции нам нужны. Естественно, нам понадобится функция, которая возвращает индекс в первый placeholder:

String => Int

Но мы также хотим пропустить любую часть строки, на которую мы уже посмотрели. Это означает, что нам также нужен начальный индекс:

(String, Int) => Int

Есть одна небольшая деталь. Что делать, если на другом месте? Тогда не было бы никакого индекса для возврата. Java повторно использует индекс, чтобы вернуть это исключение. Однако при выполнении функционального программирования всегда лучше вернуть то, что вы имеете в виду. И мы имеем в виду, что мы можем вернуть индекс, иначе мы не сможем. Подпись для этого такова:

(String, Int) => Option[Int]

Давайте построим эту функцию:

def indexOfPlaceholder(input: String, start: Int): Option[Int] = if (start < input.lengt) {
  input indexOf ("$", start) match {
    case -1 => None
    case index => 
      if (index + 1 < input.length && input(index + 1).isDigit)
        Some(index)
      else
        indexOfPlaceholder(input, index + 1)
  }
} else {
  None
}

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

Чтобы пропустить местозаполнитель, нам также нужно знать длину, подпись (String, Int) => Int:

def placeholderLength(input: String, start: Int): Int = {
  def recurse(pos: Int): Int = if (pos < input.length && input(pos).isDigit)
    recurse(pos + 1)
  else
    pos
  recurse(start + 1) - start  // start + 1 skips the "$" sign
}

Далее, мы также хотим знать, что именно, индекс значения, на который стоит местозаполнитель. Подпись для этого несколько неоднозначна:

(String, Int) => Int

Первый Int - это индекс на входе, а второй - индекс в значения. Мы могли бы что-то с этим сделать, но не так легко или эффективно, поэтому пусть игнорирует его. Вот для него реализация:

def indexOfValue(input: String, start: Int): Int = {
  def recurse(pos: Int, acc: Int): Int = if (pos < input.length && input(pos).isDigit)
    recurse(pos + 1, acc * 10 + input(pos).asDigit)
  else
    acc
  recurse(start + 1, 0) // start + 1 skips "$"
}

Мы могли бы использовать длину также и добиться более простой реализации:

def indexOfValue2(input: String, start: Int, length: Int): Int = if (length > 0) {
  input(start + length - 1).asDigit + 10 * indexOfValue2(input, start, length - 1)
} else {
  0
}

В качестве примечания, использование фигурных скобок вокруг простых выражений, таких как выше, не одобряется обычным стилем Scala, но я использую его здесь, чтобы его можно было легко вставить в REPL.

Итак, мы можем получить индекс для следующего заполнителя, его длины и индекса значения. Это почти все, что необходимо для более эффективной версии replaceRecursive:

def replaceRecursive2(input: String, values: IndexedSeq[String]): String = {
  val sb = new StringBuilder(input.length)
  def recurse(start: Int): String = if (start < input.length) {
    indexOfPlaceholder(input, start) match {
      case Some(placeholderIndex) =>
        val placeholderLength = placeholderLength(input, placeholderIndex)
        sb.append(input subSequence (start, placeholderIndex))
        sb.append(values(indexOfValue(input, placeholderIndex)))
        recurse(start + placeholderIndex + placeholderLength)
      case None => sb.toString
    }
  } else {
    sb.toString
  }
  recurse(0)
}

Гораздо эффективнее и функционально, чем можно использовать StringBuilder.

ОСОЗНАНИЕ

Понимание на самом базовом уровне означает преобразование T[A] в T[B] с помощью функции A => B. Это вещь монады, но ее легко понять, когда дело доходит до коллекций. Например, я могу преобразовать List[String] имен в List[Int] длин имен с помощью функции String => Int, которая возвращает длину строки. Это понимание списка.

Существуют и другие операции, которые могут выполняться посредством понятий, заданных функциями с сигнатурами A => T[B] или A => Boolean.

Это означает, что мы должны видеть входную строку как T[A]. Мы не можем использовать Array[Char] в качестве входных данных, потому что мы хотим заменить весь placeholder, который больше, чем один char. Поэтому предлагаем такую ​​подпись типа:

(List[String], String => String) => String

Так как мы получаем вход String, нам нужна функция String => List[String], которая разделит наш вход на заполнители и не-заполнители. Я предлагаю следующее:

val regexPattern2 = """((?:[^$]+|\$(?!\d))+)|(\$\d+)""".r
def tokenize(input: String): List[String] = regexPattern2.findAllIn(input).toList

Другая проблема заключается в том, что мы получили IndexedSeq[String], но нам нужен String => String. Есть много способов обойти это, но разрешите с этим:

def valuesMatcher(values: IndexedSeq[String]): String => String = (input: String) => values(input.substring(1).toInt - 1)

Нам также нужна функция List[String] => String, но List mkString делает это уже. Так что осталось немного оставить в стороне составление всего этого:

def comprehension(input: List[String], matcher: String => String) = 
  for (token <- input) yield (token: @unchecked) match {
    case regexPattern2(_, placeholder: String) => matcher(placeholder)
    case regexPattern2(other: String, _) => other
  }

Я использую @unchecked, потому что не должно быть никакого шаблона, кроме этих двух выше, если мой шаблон регулярного выражения был построен правильно. Однако компилятор не знает этого, поэтому я использую эту аннотацию, чтобы отключить предупреждение, которое оно произведет. Если выбрано исключение, появляется ошибка в шаблоне регулярного выражения.

Конечная функция затем объединяет все:

def replaceComprehension(input: String, values: IndexedSeq[String]) =
  comprehension(tokenize(input), valuesMatcher(values)).mkString

Одна из проблем с этим решением заключается в том, что я дважды применяю шаблон регулярного выражения: один раз для разбивки строки, а другой для определения заполнителей. Другая проблема заключается в том, что List токенов - ненужный промежуточный результат. Мы можем решить это с помощью этих изменений:

def tokenize2(input: String): Iterator[List[String]] = regexPattern2.findAllIn(input).matchData.map(_.subgroups)

def comprehension2(input: Iterator[List[String]], matcher: String => String) = 
  for (token <- input) yield (token: @unchecked) match {
    case List(_, placeholder: String) => matcher(placeholder)
    case List(other: String, _) => other
  }

def replaceComprehension2(input: String, values: IndexedSeq[String]) =
  comprehension2(tokenize2(input), valuesMatcher(values)).mkString

Складывающиеся

Складывание немного похоже на рекурсию и понимание. С складыванием мы берем вход T[A], который можно понять, a B "seed" и функцию (B, A) => B. Мы понимаем список, используя функцию, всегда беря B, которая была получена из обработанного последнего элемента (первый элемент принимает семя). Наконец, мы возвращаем результат последнего постигаемого элемента.

Я признаю, что с трудом объяснил это менее чем неясным образом. То, что происходит, когда вы пытаетесь сохранить абстрактный. Я объяснил это таким образом, чтобы сигнатуры типа были понятны. Но давайте просто посмотрим тривиальный пример складчатости, чтобы понять его использование:

def factorial(n: Int) = {
  val input = 2 to n
  val seed = 1
  val function = (b: Int, a: Int) => b * a
  input.foldLeft(seed)(function)
}

Или, как однострочный:

def factorial2(n: Int) = (2 to n).foldLeft(1)(_ * _)

Хорошо, так как же мы будем решать проблему со складыванием? Результатом, конечно же, должна быть строка, которую мы хотим создать. Поэтому семя должно быть пустой строкой. Позвольте использовать результат из tokenize2 в качестве понятного ввода и сделайте следующее:

def replaceFolding(input: String, values: IndexedSeq[String]) = {
  val seed = new StringBuilder(input.length)
  val matcher = valuesMatcher(values)
  val foldingFunction = (sb: StringBuilder, token: List[String]) => {
    token match {          
      case List(_, placeholder: String) => sb.append(matcher(placeholder))
      case List(other: String, _) => sb.append(other)
    }
    sb
  }
  tokenize2(input).foldLeft(seed)(foldingFunction).toString
}

И, с этим, я заканчиваю показывать самые обычные способы, которые можно было бы сделать это в функциональной манере. Я прибегал к StringBuilder, потому что конкатенация String медленная. Если бы это было не так, я мог бы легко заменить StringBuilder на функции выше на String. Я также мог бы преобразовать Iterator в Stream и полностью избавиться от изменчивости.

Это Scala, хотя и Scala касается балансировки потребностей и средств, а не пуристических решений. Хотя, конечно, вы можете пойти пуристом.: -)

Ответ 2

Вы можете использовать стандартный стиль Java String.format с помощью твиста:

"My name is %s and I am %d years of age".format("Oxbow", 34)

В Java, конечно, это выглядело бы так:

String.format("My name is %s and I am %d years of age", "Oxbow", 34)

Основное отличие между этими двумя стилями (я предпочитаю Scala) состоит в том, что концептуально это означает, что каждая строка может считаться строкой формата в Scala (т.е. метод формата представляется методом экземпляра на класс String). Хотя это может считаться концептуально неправильным, это приводит к более интуитивно понятному и понятному коду.

Этот стиль форматирования позволяет вам форматировать числа с плавающей запятой по желанию, даты и т.д. Основная проблема заключается в том, что "привязка" между заполнителями в строке формата и аргументами носит чисто порядок, не связанный с имена (как "My name is ${name}"), хотя я не вижу, как...

interpolate("My name is ${name} and I am ${age} years of age", 
               Map("name" -> "Oxbow", "age" -> 34))

... является более читаемым, встроенным в мой код. Такие вещи гораздо полезнее для замены текста, где исходный текст встроен в отдельные файлы (например, в i18n), где вам нужно что-то вроде:

"name.age.intro".text.replacing("name" as "Oxbow").replacing("age" as "34").text

Или:

"My name is ${name} and I am ${age} years of age"
     .replacing("name" as "Oxbow").replacing("age" as "34").text

Я бы подумал, что это будет довольно легко использовать и займет всего несколько минут, чтобы написать (я не могу заставить Дэниела интерполировать компиляцию с версией Scala 2.8):

object TextBinder {
  val p = new java.util.Properties
  p.load(new FileInputStream("C:/mytext.properties"))

  class Replacer(val text: String) {
    def replacing(repl: Replacement) = new Replacer(interpolate(text, repl.map))
  }

  class Replacement(from: String, to: String) {
    def map = Map(from -> to)
  }
  implicit def stringToreplacementstr(from: String) = new {
    def as(to: String) = new Replacement(from, to)
    def text = p.getProperty(from)
    def replacing(repl: Replacement) = new Replacer(from)
  }

  def interpolate(text: String, vars: Map[String, String]) = 
    (text /: vars) { (t, kv) => t.replace("${"+kv._1+"}", kv._2)  }
}

Я, кстати, присоединяюсь к белым API-интерфейсам! Независимо от того, насколько они неэффективны!

Ответ 3

Это не прямой ответ на ваш вопрос, а скорее трюк Scala. Вы можете интерполировать строки в Scala с помощью xml:

val id = 250
val value = "some%"
<s>select col1 from tab1 where id > {id} and name like {value}</s>.text
// res1: String = select col1 from tab1 where id > 250 and name like some%

Эрик.

Ответ 4

Вы можете использовать малоизвестные "скобки QP", чтобы разграничить выражения scala в строках. Это имеет преимущество перед другими методами в том, что вы можете использовать любое выражение scala, а не просто vals/vars. Просто используйте открывающие "+ и закрывающие разделители +".

Пример:

  val name = "Joe Schmoe"
  val age = 32
  val str = "My name is "+name+" and my age is "+age+"."