Замена оценивает в строке с заполнителями в Scala

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

Может быть больше чем 2 параметра.

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

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

17
задан Gavin 2 February 2010 в 11:12
поделиться

4 ответа

Говоря строго о проблеме замены, мое предпочтительное решение - это функция, которая, вероятно, должна быть доступна в предстоящем 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

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

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

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

(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 , который мы можем получить от Последовательностей . Я не уверен, действительно ли создана новый ряд или нет - если он есть, мы могли бы свернуть наш собственный CharSequence в путь, который действует как представление в Последовательности , вместо создания нового Последовательностей . Уверенные, что мы можем легко изменить это, если потребуется, я продолжу, предполагая, что это не так.

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

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
}

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

Чтобы пропустить местозаполнитель, нам также нужно знать его длину, сигнатуру (Последовательность, 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.

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

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 , что известно как функтор. Это легко понять, когда речь идет о коллекциях. Например, я могу преобразовать Список [Строка] имен в Список [Int] имен через функцию Строка = > Int , которая возвращает длину строки. Это понимание списка.

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

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

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

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

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

Другая проблема состоит в том, что мы получили IndexedSeq [Последовательность] , но нам нужен Последовательностям = > Последовательности . Существует много способов, но давайте согласимся с этим:

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

Нам также нужна функция Список [Последовательность] = > Последовательность ,Но Список 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 , потому что не должно быть никакого образца, кроме этих двух выше, если мой regex образцом был построен правильно. Компилятор, однако, не знает этого, поэтому я использую эту аннотацию, чтобы замолчать предупреждение, которое он выдаст. Если возникает исключение, в образце regex есть ошибка.

Итак, конечная функция объединяет все это:

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

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

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

FOLDING

Folding немного похож и на рекурсию, и на понимание. При сворачивании берем ввод T [A] , который можно понять, B «начальное число» и функцию (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 , потому что конкатенация Последовательностей медленная. Если это не так, я мог бы легко заменить StringBuilder в функциях выше на String . Я также могу преобразовать Итератор в поток и полностью исключить возможность изменения.

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

29
ответ дан 30 November 2019 в 10:49
поделиться

Вы можете использовать стандартную 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's), это то, что концептуально это означает, что каждая строка может рассматриваться как строка формата в Scala (т. Е. Метод формата, по-видимому, является методом экземпляра на Строка класс). В то время как это может быть утверждается, что это концептуально неправильно, это приводит к более интуитивно понятному и читаемому коду.

Этот стиль форматирования позволяет форматировать номера с плавающей точкой, как вы пожелаете, даты и т. Д. Основная проблема с этим, заключается в том, что «привязка» между заполнителями в строке формата и аргументами является чисто на основе порядка, не связана с Имена каким-либо образом (вроде «Мое имя - это $ {имя}» ), хотя я не вижу, как ...

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

Я AA Sucker для свободных API ! Независимо от того, насколько они не являются неверными!

14
ответ дан 30 November 2019 в 10:49
поделиться

Вы можете использовать маленькие известные «кронштейны QP» для разграничения Scala Sepressions внутри строк. Это имеет преимущество перед другими методами, в том, что вы можете использовать любую Scala Expression, а не просто простые Vals / vars. Просто используйте отверстие + и закрытие + « разделители кронштейна.

Пример:

  val name = "Joe Schmoe"
  val age = 32
  val str = "My name is "+name+" and my age is "+age+"."
1
ответ дан 30 November 2019 в 10:49
поделиться

Это не прямой ответ на ваш вопрос, а скорее трюк со скалой. Вы можете интерполировать строки в 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%

Eric.

3
ответ дан 30 November 2019 в 10:49
поделиться
Другие вопросы по тегам:

Похожие вопросы: