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:
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:
- Vuelva a colocar el primer marcador de posición
- 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. :-)
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