source - scala tutorial




Sustituir valores en una cadena con marcadores de posición en Scala (4)

Acabo de comenzar a usar Scala y deseo comprender mejor el enfoque funcional para resolver problemas. Tengo pares de cadenas, el primero tiene marcadores de posición para el parámetro y su par tiene los valores para sustituir. por ejemplo, "seleccione col1 en tab1 donde id> $ 1 y nombre como $ 2" parámetros: $ 1 = '250', $ 2 = 'algo%' "

Puede haber muchos más de 2 parámetros.

Puedo construir la cadena correcta al pasar y usar regex.findAllIn (línea) en cada línea y luego pasar por los iteradores para construir la sustitución, pero esto parece bastante poco elegante e impulsado por el procedimiento.

¿Alguien podría dirigirme hacia un enfoque funcional que sea más ordenado y menos propenso a errores?


Esta no es una respuesta directa a tu pregunta, sino más bien un truco de Scala. Puedes interpolar cadenas en Scala usando 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


Hablando estrictamente del problema de reemplazo, mi solución preferida es la habilitada por una función que probablemente debería estar disponible en la próxima Scala 2.8, que es la capacidad de reemplazar los patrones de expresiones regulares utilizando una función. Usándolo, el problema puede reducirse a esto:

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

Lo que reduce el problema a lo que realmente pretende hacer: reemplace todos los patrones de $ N por el correspondiente enésimo valor de una lista.

O, si realmente puede establecer los estándares para su cadena de entrada, podría hacerlo así:

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

Si eso es todo lo que quieres, puedes parar aquí. Sin embargo, si está interesado en cómo resolver tales problemas de una manera funcional, en ausencia de funciones inteligentes de la biblioteca, continúe leyendo.

Pensar funcionalmente sobre eso significa pensar en la función. Tienes una cadena, algunos valores y quieres una cadena de vuelta. En un lenguaje funcional estático, eso significa que quieres algo como esto:

(String, List[String]) => String

Si consideramos que esos valores se pueden usar en cualquier orden, podemos solicitar un tipo más adecuado para eso:

(String, IndexedSeq[String]) => String

Eso debería ser lo suficientemente bueno para nuestra función. Ahora, ¿cómo desglosamos el trabajo? Hay algunas formas estándar de hacerlo: recursión, comprensión, plegamiento.

RECURSION

Empecemos con la recursión. La recursión significa dividir el problema en un primer paso y luego repetirlo sobre los datos restantes. Para mí, la división más obvia aquí sería la siguiente:

  1. Reemplace el primer marcador de posición
  2. Repita con los restantes marcadores de posición.

En realidad, es bastante sencillo de hacer, así que vamos a entrar en más detalles. ¿Cómo reemplazo el primer marcador de posición? Una cosa que no se puede evitar es que necesito saber qué es ese marcador de posición, porque necesito obtener el índice en mis valores. Así que necesito encontrarlo:

(String, Pattern) => String

Una vez encontrado, puedo reemplazarlo en la cadena y repetir:

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
}

Eso es ineficiente, porque produce repetidamente nuevas cadenas, en lugar de solo concatenar cada parte. Tratemos de ser más inteligentes al respecto.

Para construir una cadena de manera eficiente a través de la concatenación, necesitamos usar StringBuilder . También queremos evitar la creación de nuevas cadenas. StringBuilder puede aceptar CharSequence , que podemos obtener de String . No estoy seguro de si realmente se creó una nueva cadena o no, si lo está, podríamos CharSequence nuestra propia CharSequence de una manera que actúe como una vista en la String , en lugar de crear una nueva String . Con la seguridad de que podemos cambiarlo fácilmente si es necesario, procederé en el supuesto de que no lo sea.

Entonces, consideremos qué funciones necesitamos. Naturalmente, desearemos una función que devuelva el índice al primer marcador de posición:

String => Int

Pero también queremos omitir cualquier parte de la cadena que ya hemos visto. Eso significa que también queremos un índice de partida:

(String, Int) => Int

Sin embargo, hay un pequeño detalle. ¿Qué pasa si hay más marcador de posición? Entonces no habría ningún índice para volver. Java reutiliza el índice para devolver esa excepción. Sin embargo, cuando se realiza una programación funcional, siempre es mejor devolver lo que quiere decir. Y lo que queremos decir es que podemos devolver un índice, o no. La firma para eso es esta:

(String, Int) => Option[Int]

Vamos a construir esta función:

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
}

Eso es bastante complejo, principalmente para lidiar con las condiciones de los límites, como que el índice esté fuera de rango, o falsos positivos cuando se buscan marcadores de posición.

Para omitir el marcador de posición, también necesitaremos saber su longitud, firma (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
}

A continuación, también queremos saber qué es exactamente el índice del valor que representa el marcador de posición. La firma para esto es un poco ambigua:

(String, Int) => Int

El primer Int es un índice en la entrada, mientras que el segundo es un índice en los valores. Podríamos hacer algo al respecto, pero no tan fácil o eficientemente, así que ignorémoslo. Aquí hay una implementación para ello:

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 "$"
}

Podríamos haber usado la longitud también, y lograr una implementación más simple:

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
}

Como nota, el uso de corchetes en expresiones simples, como las de arriba, es mal visto por el estilo Scala convencional, pero lo uso aquí para que pueda pegarse fácilmente en REPL.

Por lo tanto, podemos obtener el índice para el siguiente marcador de posición, su longitud y el índice del valor. Eso es prácticamente todo lo necesario para una versión más eficiente de 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)
}

Mucho más eficiente, y tan funcional como uno puede estar usando StringBuilder .

COMPRENSIÓN

Comprensiones, en su nivel más básico, significa transformar T[A] en T[B] dada una función A => B Es una cuestión de la mónada, pero puede entenderse fácilmente cuando se trata de colecciones. Por ejemplo, puedo transformar una List[String] de nombres en una List[Int] de longitudes de nombre a través de una función String => Int que devuelve la longitud de una cadena. Esa es una lista de comprensión.

Hay otras operaciones que pueden realizarse a través de comprensiones, funciones dadas con firmas A => T[B] o A => Boolean .

Eso significa que necesitamos ver la cadena de entrada como una T[A] . No podemos usar Array[Char] como entrada porque queremos reemplazar todo el marcador de posición, que es más grande que un solo char. Propongamos, por lo tanto, este tipo de firma:

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

Ya que la entrada que recibimos es String , primero necesitamos una función String => List[String] , que dividirá nuestra entrada en marcadores de posición y no marcadores de posición. Propongo esto:

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

Otro problema que tenemos es que obtuvimos un IndexedSeq[String] , pero necesitamos un String => String . Hay muchas maneras de evitarlo, pero resolvamos esto:

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

También necesitamos una función List[String] => String , pero mkString List ya lo hace. Así que no queda mucho por hacer aparte de componer todo esto:

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
  }

Uso @unchecked porque no debería haber ningún patrón distinto de estos dos anteriores, si mi patrón de expresiones regulares se construyó correctamente. Sin embargo, el compilador no lo sabe, así que uso esa anotación para silenciar la advertencia que produciría. Si se lanza una excepción, hay un error en el patrón de expresiones regulares.

La función final, entonces, unifica todo eso:

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

Un problema con esta solución es que aplico el patrón de expresiones regulares dos veces: una para separar la cadena y la otra para identificar los marcadores de posición. Otro problema es que la List de tokens es un resultado intermedio innecesario. Podemos resolver eso con estos cambios:

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

PLEGABLE

El plegado es un poco similar tanto a la recursión como a la comprensión. Con el plegado, tomamos una entrada T[A] que se puede comprender, una "semilla" B y una función (B, A) => B Comprendemos la lista usando la función, tomando siempre la B que resultó del último elemento procesado (el primer elemento toma la semilla). Finalmente, devolvemos el resultado del último elemento comprendido.

Admito que apenas podía explicarlo de una manera menos oscura. Eso es lo que pasa cuando intentas mantenerte abstracto. Lo expliqué de esa manera para que las firmas de tipo involucradas queden claras. Pero veamos un ejemplo trivial de plegado para entender su uso:

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

O, como una sola línea:

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

Ok, entonces, ¿cómo vamos a resolver el problema con el plegado? El resultado, por supuesto, debe ser la cadena que queremos producir. Por lo tanto, la semilla debe ser una cadena vacía. Usemos el resultado de tokenize2 como entrada comprensible, y hagamos esto:

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
}

Y, con eso, termino mostrando las formas más usuales en que uno podría hacerlo de una manera funcional. He recurrido a StringBuilder porque la concatenación de String es lenta. Si ese no fuera el caso, podría reemplazar fácilmente a StringBuilder en las funciones anteriores por String . También podría convertir Iterator en un Stream y eliminar completamente la mutabilidad.

Sin embargo, este es Scala y Scala trata de equilibrar necesidades y medios, no de soluciones puristas. Aunque, por supuesto, eres libre de ir purista. :-)


Puede utilizar el estilo estándar de Java String.format con un toque:

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

En Java, por supuesto, esto habría parecido:

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

La principal diferencia entre estos dos estilos (prefiero mucho los de Scala) es que, conceptualmente, esto significa que cada String puede considerarse una cadena de formato en Scala (es decir, el método de formato parece ser un método de instancia en la clase String ). Si bien se puede argumentar que esto es conceptualmente incorrecto, conduce a un código más intuitivo y legible.

Este estilo de formato le permite formatear números de punto flotante como desee, fechas, etc. El problema principal es que el "enlace" entre los marcadores de posición en la cadena de formato y los argumentos se basa puramente en el orden, no en nombres. manera (como "My name is ${name}" ) aunque no veo cómo ...

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

... es más legible incrustado en mi código. Este tipo de cosas es mucho más útil para el reemplazo de texto donde el texto de origen está incrustado en archivos separados (por ejemplo, en i18n ) donde querría algo como:

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

O:

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

Pensaría que esto sería bastante fácil de usar y tomaría unos minutos para escribir (parece que no puedo hacer que se compile la interpolación de Daniel con la versión Scala 2.8 que tengo):

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

Soy un tonto para APIs fluidas por cierto! ¡No importa qué tan deficientes sean!






functional-programming