2010-02-02 13 views
17

Acabo de empezar a usar Scala y deseo comprender mejor el enfoque funcional para la resolución de problemas. Tengo pares de cadenas, el primero tiene marcadores de posición para el parámetro y su par tiene los valores para sustituir. p.ej. "select col1 de Tab1 donde id> $ 1 y $ 2 de renombre como" "parámetros: $ 1 = '250', $ 2 = 'algunos%'"Sustituir valores en una cadena con marcadores de posición en Scala

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

Puedo construir la cadena correcta al recorrer 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 apuntarme hacia un enfoque funcional que sea más ordenado y menos propenso a errores?

+2

http://dcsobral.blogspot.com/2010/01/string-interpolation-in-scala-with.html Aunque esta característica es todavía no en 2.8.0-beta1. Puede usar la última instantánea para ello. – F0RR

Respuesta

29

Hablando estrictamente para el problema de reemplazo, mi solución preferida es una habilitada por una función que probablemente debería estar disponible en el próximo Scala 2.8, que es la capacidad de reemplazar patrones regex utilizando una función.Al usarlo, el problema se reduce a esto:

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

que reduce el problema a lo que realmente la intención de hacer: reemplazar todos los $ N patrones por el correspondiente valorenésimo de una lista.

O, si en realidad se puede establecer las normas para su cadena de entrada, puede hacerlo de esta manera:

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

Si eso es todo lo que desea, puede parar aquí. Sin embargo, si está interesado en cómo resolver estos problemas de manera funcional, sin funciones de biblioteca inteligentes, continúe leyendo.

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

(String, List[String]) => String 

Si se considera que esos valores se pueden usar en cualquier orden, podemos pedir un tipo más adecuado para ello:

(String, IndexedSeq[String]) => String 

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

RECURSION

Vamos a empezar con la recursividad. 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 sería la siguiente:

  1. Vuelva a colocar el primer marcador de posición
  2. Repita con los símbolos restantes

que en realidad es bastante recta hacia adelante para hacer, así que vamos a 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 a partir de él. Así que tengo que encontrarlo:

(String, Pattern) => String 

Una vez encontrado, que puede reemplazarla en la cuerda 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, ya que en repetidas ocasiones produce nuevas cadenas, en lugar de sólo la concatenación de cada uno parte. Tratemos de ser más inteligentes al respecto.

Para construir eficientemente una cadena a través de la concatenación, necesitamos usar StringBuilder. También queremos evitar la creación de nuevas cadenas. StringBuilder puede acepta CharSequence, que podemos obtener de String. No estoy seguro de si se ha creado o no una nueva cadena, si es así, podríamos rodar nuestra propia CharSequence de una manera que actúe como una vista en String, en lugar de crear una nueva String. Asegurándonos de que podemos cambiar esto fácilmente si es necesario, procederé asumiendo que no es así.

Entonces, consideremos qué funciones necesitamos.Naturalmente, vamos a querer una función que devuelve el índice en el primer marcador de posición:

String => Int 

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

(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 regresar. Java vuelve a utilizar el índice para devolver esa excepción. Sin embargo, al hacer programación funcional, siempre es mejor devolver lo que quieres decir. Y lo que queremos decir es que puede devolver un índice, o puede que no. La firma de ello es la siguiente:

(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, sobre todo para hacer frente a las condiciones de frontera, como el índice de estar fuera de alcance o falsos positivos cuando se mira para marcadores de posición.

Para saltar el marcador de posición, también necesitaremos saber su longitud, la 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 del marcador de posición está de pie para . La firma de esto es un poco ambigua:

(String, Int) => Int 

La primera 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 de manera fácil ni eficiente, así que ignorémoslo. He aquí una aplicació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 utilizado 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 una nota, mediante llaves alrededor de las expresiones simples, como el anterior, se mal visto por el estilo clásico de Scala, 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

de comprensión, en su nivel más básico, significa transformar T[A] en T[B] dada una función A => B. Es una cosa de la mónada, pero se puede entender fácilmente cuando se trata de colecciones. Por ejemplo, puedo transformar un List[String] de nombres en un 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.

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

Eso significa que tenemos que ver la cadena de entrada como T[A]. No podemos usar Array[Char] como entrada porque queremos reemplazar el marcador de posición completo, que es más grande que un solo carácter. Vamos a proponer, por lo tanto, este tipo de firma:

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

Ya que la entrada que recibimos es String, necesitamos una función String => List[String] primera, que dividirá nuestra entrada en los 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 tenemos un IndexedSeq[String], pero necesitamos un String => String. Hay muchas maneras de evitar eso, sino que vamos a resolver con 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 List 's mkString ya hace eso. Así que hay poco que hacer a un lado la composición de todas estas cosas:

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 
    } 

utilizo @unchecked porque hay no debe ser cualquier patrón que no sean estos dos arriba, si mi patrón de expresión se ha construido correctamente. El compilador no lo sabe, sin embargo, entonces 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 expresión dos veces: una vez para romper la cadena, y la otra para identificar los marcadores de posición. Otro problema es que el List de tokens es un resultado intermedio innecesario. Podemos resolver que 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 

PLEGABLES

plegable es un poco similar a tanto la recursividad y la comprensión. Con 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 el B que resultó del último elemento procesado (el primer elemento toma la semilla). Finalmente, devolvemos el resultado del último elemento comprendido.

Debo admitir que apenas pude explicarlo de una manera menos oscura. Eso es lo que sucede cuando tratas de mantenerte abstracto. Lo expliqué de esa manera para que las firmas de tipo involucradas se aclararan. Pero vamos a ver 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, así que ¿cómo podemos ir sobre la solución del problema con el plegado? El resultado, por supuesto, debería ser la cadena que queremos producir.Por lo tanto, la semilla debe ser una cadena vacía. Vamos a utilizar el resultado de tokenize2 como el input comprensible, y hacer 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, acabo de mostrar las formas más habituales se podría ir sobre esto 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 StringBuilder en las funciones anteriores por String. También podría convertir Iterator en un Stream, y eliminar completamente la mutabilidad.

Esta es Scala, sin embargo y Scala es acerca de las necesidades y los medios de equilibrio, no de soluciones puristas. Aunque, por supuesto, eres libre de ser purista. :-)

+0

Gracias por las explicaciones detalladas que me ayudó a resolver el problema real que iba a reproducir los registros de PostgreSQL que contienen miles de simple y sentencias SQL parametrizadas y terminé usando el código del ejemplo plegable. – Gavin

14

Puede utilizar el standard Java String.format style con una peculiaridad:

"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 (me gusta mucho más Scala) es que conceptualmente esto significa que cada String se puede considerar 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 podría argumentarse que es conceptualmente incorrecto, conduce a un código más intuitivo y legible.

Este estilo de formateo le permite formatear números de punto flotante como desee, fechas, etc. El problema principal es que la "vinculación" entre los marcadores de posición en el formato de cadena y los argumentos está puramente basada en orden, no relacionada a los nombres de ninguna 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 la sustitución de texto en el que el texto original se incrusta en archivos separados (en i18n por ejemplo) en el que quiera 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 

me gustaría pensar que esto sería bastante fácil de usar y tomar sólo unos minutos para escribir (que parece que no puede conseguir interpolación de Daniel para compilar con la versión 2.8 Scala 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) } 
} 

¡Por cierto, soy una fanática de las API fluidas! ¡No importa cuán imperfectos sean!

+3

Parece que muchas personas no conocen el modo de referencia posicional en las cadenas de formato de Java. Por ejemplo: '% 3 $ d'. En esta especificación de formato, se hace referencia explícitamente al tercer argumento sin considerar otras especificaciones de formato en la misma cadena de formato. Naturalmente, también pueden aparecer múltiples referencias a cualquier argumento dado. –

+0

Randall: buen punto, pero con muchos parámetros, esto puede ser muy poco claro. fechas de formato como este puede ser una pesadilla: '% 1% 1 $ tY $ tm% 1 $ td' –

+0

Es ciertamente lejos de ser ideal. El blog de Daniel String Interpolation muestra una alternativa elegante. –

3

Ésta no es una respuesta directa a su pregunta, sino más bien un truco Scala. Se puede interpolar cadenas en Scala utilizando 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.

1

Puede utilizar los soportes poco conocidos "QP" para delimitar expresiones Scala dentro de cadenas. Esto tiene una ventaja sobre otros métodos en que puedes usar cualquier expresión de scala, no solo vals/vars simples. Simplemente use una abertura "+ y cierre +" delimitadores del soporte.

Ejemplo:

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