Gav*_*vin 17 functional-programming scala
我刚开始使用Scala,希望更好地理解解决问题的功能方法.我有一对字符串,第一个字符串有参数的占位符,它的对有要替换的值.例如"从tab1中选择col1,其中id> $ 1,名称如$ 2""参数:$ 1 ='250',$ 2 ='some%'"
可能有多于2个参数.
我可以通过逐步执行并在每一行上使用regex.findAllIn(line)构建正确的字符串,然后通过迭代器来构造替换,但这似乎相当不优雅且程序驱动.
任何人都可以指出我的功能方法更整洁,更不容易出错吗?
Dan*_*ral 29
严格说来是替换问题,我首选的解决方案是一个功能,该功能应该可以在即将推出的Scala 2.8中使用,它可以使用函数替换正则表达式模式.使用它,问题可以减少到这个:
def replaceRegex(input: String, values: IndexedSeq[String]) =
"""\$(\d+)""".r.replaceAllMatchesIn(input, {
case Regex.Groups(index) => values(index.toInt)
})
Run Code Online (Sandbox Code Playgroud)
这将问题简化为您实际要执行的操作:将所有$ N模式替换为列表的相应第N个值.
或者,如果您实际上可以为输入字符串设置标准,则可以这样做:
"select col1 from tab1 where id > %1$s and name like %2$s" format ("one", "two")
Run Code Online (Sandbox Code Playgroud)
如果这就是你想要的,你可以在这里停下来.但是,如果您对如何以功能性方式解决此类问题感兴趣,如果没有聪明的库函数,请继续阅读.
从功能上思考它意味着思考功能.你有一个字符串,一些值,你想要一个字符串.在静态类型的函数式语言中,这意味着你想要这样的东西:
(String, List[String]) => String
Run Code Online (Sandbox Code Playgroud)
如果考虑到这些值可以按任何顺序使用,我们可能会要求更适合的类型:
(String, IndexedSeq[String]) => String
Run Code Online (Sandbox Code Playgroud)
这应该足够我们的功能.现在,我们如何分解工作?有几种标准方法:递归,理解,折叠.
递推
让我们从递归开始.递归意味着将问题分成第一步,然后在剩余数据上重复.对我来说,这里最明显的部门如下:
这实际上是非常简单的,所以让我们进一步了解细节.如何更换第一个占位符?有一点是无法避免的,我需要知道占位符是什么,因为我需要从中获取索引到我的值中.所以我需要找到它:
(String, Pattern) => String
Run Code Online (Sandbox Code Playgroud)
一旦找到,我可以在字符串上替换它并重复:
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
}
Run Code Online (Sandbox Code Playgroud)
这是低效的,因为它反复产生新的字符串,而不是仅仅连接每个部分.让我们试着更加聪明一点.
要通过连接有效地构建字符串,我们需要使用StringBuilder.我们还想避免创建新字符串.StringBuilder可以接受CharSequence,我们可以得到String.我不知道,如果实际创建或不是一个新字符串-如果是这样,我们可以推出我们自己CharSequence在充当视图分成的方式String,而不是创建一个新的String.确保我们可以根据需要轻松更改,我会继续假设它不是.
那么,让我们考虑一下我们需要什么功能.当然,我们需要一个将索引返回到第一个占位符的函数:
String => Int
Run Code Online (Sandbox Code Playgroud)
但是我们也想跳过我们已经看过的字符串的任何部分.这意味着我们还需要一个起始索引:
(String, Int) => Int
Run Code Online (Sandbox Code Playgroud)
但是有一个小细节.如果还有占位符怎么办?然后就没有任何索引可以返回.Java重用索引来返回该异常.然而,在进行函数式编程时,最好还是返回你的意思.我们的意思是我们可能会返回一个索引,或者我们可能不会.签名是这样的:
(String, Int) => Option[Int]
Run Code Online (Sandbox Code Playgroud)
让我们构建这个函数:
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
}
Run Code Online (Sandbox Code Playgroud)
这相当复杂,主要是为了处理边界条件,例如索引超出范围,或者在寻找占位符时出现误报.
要跳过占位符,我们还需要知道它的长度,签名(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
}
Run Code Online (Sandbox Code Playgroud)
接下来,我们还想知道占位符所代表的值的索引.这个签名有点含糊不清:
(String, Int) => Int
Run Code Online (Sandbox Code Playgroud)
第一个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 "$"
}
Run Code Online (Sandbox Code Playgroud)
我们也可以使用长度,并实现更简单的实现:
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
}
Run Code Online (Sandbox Code Playgroud)
作为一个注释,在简单表达式周围使用大括号,如上所述,是传统的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)
}
Run Code Online (Sandbox Code Playgroud)
效率更高,功能更强大StringBuilder.
理解
在最基本的层面上,理解意味着转变T[A]为T[B]给定的功能A => B.这是一个monad的东西,但它在收集时很容易理解.例如,我可以转换一个List[String]名字为List[Int]通过函数名长度的String => Int返回字符串的长度.这是列表理解.
在给定具有签名A => T[B]或函数的函数的情况下,还可以通过理解来完成其他操作A => Boolean.
这意味着我们需要将输入字符串视为一个T[A].我们不能Array[Char]用作输入,因为我们想要替换整个占位符,它大于单个char.因此,让我们提出这种类型的签名:
(List[String], String => String) => String
Run Code Online (Sandbox Code Playgroud)
由于我们收到的输入是String,我们String => List[String]首先需要一个函数,它将我们的输入分为占位符和非占位符.我建议这个:
val regexPattern2 = """((?:[^$]+|\$(?!\d))+)|(\$\d+)""".r
def tokenize(input: String): List[String] = regexPattern2.findAllIn(input).toList
Run Code Online (Sandbox Code Playgroud)
我们遇到的另一个问题是我们得到了一个IndexedSeq[String],但我们需要一个String => String.有很多方法,但让我们解决这个问题:
def valuesMatcher(values: IndexedSeq[String]): String => String = (input: String) => values(input.substring(1).toInt - 1)
Run Code Online (Sandbox Code Playgroud)
我们还需要一个功能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
}
Run Code Online (Sandbox Code Playgroud)
我使用的@unchecked是因为除了上面这两个之外不应该有任何模式,如果我的正则表达式模式是正确构建的.但是,编译器不知道,所以我使用该注释来静默它将产生的警告.如果抛出异常,则正则表达式模式中存在错误.
然后,最终的功能统一了所有:
def replaceComprehension(input: String, values: IndexedSeq[String]) =
comprehension(tokenize(input), valuesMatcher(values)).mkString
Run Code Online (Sandbox Code Playgroud)
这个解决方案的一个问题是我应用了两次正则表达式模式:一次打破字符串,另一次识别占位符.另一个问题是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
Run Code Online (Sandbox Code Playgroud)
折叠
折叠有点类似于递归和理解.通过折叠,我们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)
}
Run Code Online (Sandbox Code Playgroud)
或者,作为一个单行:
def factorial2(n: Int) = (2 to n).foldLeft(1)(_ * _)
Run Code Online (Sandbox Code Playgroud)
好的,那么我们如何解决折叠问题呢?当然,结果应该是我们想要生成的字符串.因此,种子应该是空的字符串.让我们使用结果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
}
Run Code Online (Sandbox Code Playgroud)
并且,通过这种方式,我完成了以功能性方式展示最常用的方式.我求助于StringBuilder因为连接String缓慢.如果不是这样的话,我可以轻松地替换StringBuilder上面的函数String.我也可以转换Iterator成一个Stream,并完全消除可变性.
这是Scala,而Scala是关于平衡需求和手段,而不是纯粹的解决方案.当然,你可以自由地去纯粹主义.:-)
oxb*_*kes 14
您可以使用标准Java String.format样式:
"My name is %s and I am %d years of age".format("Oxbow", 34)
Run Code Online (Sandbox Code Playgroud)
在Java当然这看起来像:
String.format("My name is %s and I am %d years of age", "Oxbow", 34)
Run Code Online (Sandbox Code Playgroud)
这两种样式之间的主要区别(我更喜欢Scala)是概念上这意味着每个String都可以被认为是Scala中的格式字符串(即格式方法似乎是类中的实例方法String).虽然这可能被认为在概念上是错误的,但它会导致更直观和可读的代码.
这种格式化样式允许您根据需要格式化浮点数,日期等.它的主要问题是格式字符串中的占位符与参数之间的"绑定"纯粹基于顺序,与任何名称无关.方式(像"My name is ${name}")虽然我没有看到如何...
interpolate("My name is ${name} and I am ${age} years of age",
Map("name" -> "Oxbow", "age" -> 34))
Run Code Online (Sandbox Code Playgroud)
...在我的代码中嵌入了更多可读性.这种东西对于文本替换更有用,其中源文本嵌入在单独的文件中(例如在i18n中),您需要这样的文件:
"name.age.intro".text.replacing("name" as "Oxbow").replacing("age" as "34").text
Run Code Online (Sandbox Code Playgroud)
要么:
"My name is ${name} and I am ${age} years of age"
.replacing("name" as "Oxbow").replacing("age" as "34").text
Run Code Online (Sandbox Code Playgroud)
我认为这很容易使用,只需几分钟即可编写(我似乎无法使用我的Scala 2.8版本编译Daniel的插值):
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) }
}
Run Code Online (Sandbox Code Playgroud)
顺便说一句,我是一个流畅的API的傻瓜!无论他们多么无形!
| 归档时间: |
|
| 查看次数: |
13750 次 |
| 最近记录: |