您的位置:首页 > 其它

Programming In Scala笔记-第九章、控制抽象

2016-10-04 00:16 141 查看
  本章主要讲解在Scala中如何使用函数值来自定义新的控制结构,并且介绍Curring和By-name参数的概念。

一、减少重复代码

1、重复代码的场景描述

  前面定义的函数,将实现某功能的代码封装到一起形成一个特定功能的代码块。那么,正常情况下,各函数之间有可能会有部分逻辑是相同的。不好理解的话,看看下面的代码。

object FileMatcher {
private def filesHere = (new java.io.File(".")).listFiles

def filesEnding(query: String) =
for (file <- filesHere; if file.getName.endsWith(query))
yield file

def filesContaining(query: String) =
for (file <- filesHere; if file.getName.contains(query))
yield file

def filesRegex(query: String) =
for (file <- filesHere; if file.getName.matches(query))
yield file
}


  上面代码中定义了一个文件操作对象
FileMatcher
,其中有三个函数,
filesEnding
是表示筛选出其中以特定字符结尾的文件,
filesContaining
表示筛选出其中包含特定字符的文件,
filesRegex
表示根据正则表达式匹配。仔细看这三个函数,大体上的代码都是相同的,只有部分特殊逻辑不相同。

2、使用函数参数

  幸好Scala为这种情况提供了一个简化代码的策略,即定义一个方法,该方法传入两个参数,第一个参数为文件名,第二个参数为一个方法变量。比如

def filesMatching(query: String, matcher: (String, String) => Boolean) = {
for (file <- filesHere; if matcher(file.getName, query))
yield file
}


其中
filesMatching
方法的第二个参数是一个函数,输入参数为两个
String
类型变量,返回值为一个
Boolean
类型的值。

  如果想要基于该函数实现一个
filesEnding
函数,第一反应是写如下一段代码

def filesEnding(query: String) =
filesMatching(query, _.endsWith(_))


这里的
filesEnding
函数接收一个参数
query
,以及一个函数参数
_.endsWith(_)
。该函数参数使用了下划线的形式来表示输入参数,表示该函数参数接收两个参数,其中第一个参数作用在第一个下划线处,第二个参数作用在第二个下划线处,即该函数参数完整的表现形式是

(fileName: String, query: String) => fileName.endsWith(query)


  进一步,我们看到,
filesMatching
函数在调用该函数参数时,传入一个函数名,然后将传入的query参数不作任何处理直接传递到
_.endsWith
的函数参数中。从前面有关闭包部分的知识我们知道这里其实也是可以进行简化的,进一步简化的
filesEnding
函数如下所示

def filesEnding(query: String) =
filesMatching(_.endsWith(query))


  这里解释一下,
_.endsWith(query)
根据传入的
query
参数的内容,动态的生成一个闭包函数,该函数只接收一个
String
类型的参数,并且该参数实质上应该是传入一个文件名。此时
filesMatching
函数的也需要重写,可以参考下面一段代码。

3、代码简化

  那么,对于最前面那段代码中的功能,可以将代码简化成如下形式。将逻辑相同的部分进一步剥离,形成一个函数
finesMatching
,然后其他三个函数,分别调用该函数,传入一个文件名处理函数即可。注意这里的
filesMatching
函数与前面的不同之处。

object FileMatcher {
private def filesHere = (new java.io.File(".")).listFiles

def filesMatching(matcher: String => Boolean) = {
for (file <- filesHere; if matcher(file.getName))
yield file
}

def filesEnding(query: String) = filesMatching(_.endsWith(query))

def filesContaining(query: String) =
filesMatching(_.contains(query))

def filesRegex(query: String) =
filesMatching(_.matches(query))
}


二、简化客户端代码

  这里也是和第一节中的类似,通过调用接收函数参数的函数来实现代码简化,只不过第一节中的函数是用户自定义的,而这里的函数是Scala API提供的。

1、直观的定义ccontainsNeg方法

  接下来通过自定义一个
containsNeg
方法来展开本节。
containsNeg
方法主要作用于Collection类型的对象上,用于判断List[Int]类型对象中是否包含小于0的元素。那么,最直观的实现方法是定义如下函数,在其中定义一个
var
变量,初始值为
false
,依次循环取出该List[Int]对象中的元素与给定元素进行比较,如果遇到小于0的元素,将该
var
变量更新为
true
,如下所示

def containsNeg(nums: List[Int]): Boolean = {
var exists = false
for (num <- nums)
if (num < 0)
exists = true
exists
}

containsNeg(List(1, 2, 3, 4))
containsNeg(List(1, 2, -3, 4))


  运行结果如下,

  


2、使用exists方法接收函数参数简化containsNeg方法

  重新定义一个
containsNeg
方法,通过调用Collection对象的
exists
方法,传入一个函数,如下所示

def containsNeg(nums: List[Int]) = nums.exists(_ < 0)

containsNeg(Nil)
containsNeg(List(0, -1, -2))


  运行结果如下,

  


  

  这里的
exists
方法,是Scala语言提供的一个具有特殊功能的控制结构语句,有点类似于
while
for
循环,但是用户可以根据需求传入一个自定义的函数用来实现更多功能。

  这里和第二节中的
filesMatching
方法不同的是,
filesMatching
方法是用户自定义的,而
exists
方法是Scala API中提供的。在Scala API中还有一些类似于
exists
的方法,在写代码时最好多使用这些函数,以便简化代码。

三、Curring

  在Scala中,除了可以使用之前介绍的Scala提供的控制结构之外,还可以用户自定义控制结构。在自定义控制结构之前,需要首先明确一个概念——柯里化(curring)。

  柯里化的函数可以接收多个参数列表,而不是之前的那些函数中的一个参数列表。下面分别举例了一个非柯里化函数和一个柯里化函数,实现相同的求两个整数之和的功能。

1、非柯里化函数

  按照之前的函数定义风格,定义一个求和函数。

def plainOldSum(x: Int, y: Int) = x + y

plainOldSum(1, 2)


  运行结果如下:

  


  

2、柯里化函数

  与上面的函数对比,这里列举出一个柯里化的求和函数。可以看到柯里化函数与非柯里化函数的明显区别是函数名后有两个括号,分别接收一个参数列表。

def curriedSum(x: Int)(y: Int) = x + y

curriedSum(1)(2)


  运行结果如下:

  


3、柯里化函数分析

  在调用
curriedSum
函数时,实际上会连续执行两个传统的函数。第一个函数接收一个
Int
型参数
x
,得到一个函数值,然后改函数值接收另一个
Int
型参数
y
,得到最终的计算结果。

  
curriedSum
的执行过程可以由如下两个函数模拟得到。

def first(x: Int) = (y: Int) => x + y

val second = first(1)
second(3)


  执行过程如下

  


  注意,上面的
first
second
只是对柯里化函数
curriedSum
执行过程的模拟,如果需要从
curriedSum
函数直接得到类似于
second
的函数,需要按部分应用函数的形式来表示,将第二个函数列表用下划线代替,如下所示

val onePlus = curriedSum(1)_

onePlus(2)


  执行结果如下,部分应用函数的下划线和前面的函数调用之间可以加空格也可以不加空格。

  


  前面介绍部分应用函数,比如
println _
时明确强调需要在下划线前加空格,而在
curriedSum(1)_
时不需要空格。这是由于在Scala中
println_
可以合法表示一个变量名,而
curriedSum(1)_
明显不是一个变量名,Scala编译器在遇到这种表示法时不会出现歧义。

四、自定义控制结构

  在Scala中,通过定义接收函数参数的函数可以定义新的控制结构。上一节中的柯里化的概念主要将作用于本节最后一部分,将代码中的圆括号改成花括号,使自定义控制结构的使用风格更加类似于Scala提供的控制结构。

1、简单的控制结构示例

  比如下面的
twice
函数的参数列表中是一个函数参数以及一个Double类型的变量,当调用
twice
时会自动将传入的函数参数在传入的变量值上执行两遍。

def twice(op: Double => Double, x: Double) = op(op(x))

twice(_ + 1, 5)


  运行结果如下,

  


2、借贷模式(loan pattern)

  所以,当在写代码时发现部分代码结构频繁使用到时,就需要考虑将这部分逻辑抽取出来形成一个自定义控制结构。在前面章节中提到的
filesMatching
函数是自定义控制结构的一种简单实现,接下来将实现一个稍微复杂一点的逻辑。

  下面代码打开一个文件,操作该文件,最终关闭该文件的连接。

def withPrintWriter(file: File, op: PrintWriter => Unit) {
val writer = new PrintWriter(file)
try {
op(writer)
} finally {
writer.close()
}
}

withPrintWriter(new File("date.txt"), writer => writer.println(new java.util.Date))


  使用上面这种代码格式的一个好处是,定义好
withPrintWtirer
的整体结构后,用户只需要实现对文件的操作逻辑即可,不再需要显示的执行文件关闭的操作,并且也能保证文件操作完毕后一定会在
finally
中关闭该文件的连接。

  这里也体现出了一种借贷模式的思想,在
withPrintWriter
方法中,
writer
获取到该文件的连接,然后将该文件借给操作函数
op
,待
op
用完该文件的连接时再将该文件连接还回
withPrintWriter
方法的后续代码,比如在
finally
代码中关闭该文件连接。

3、在自定义控制结构中使用{}代替()

  回想一下
for
while
循环,在函数体部分使用的是花括号
{}
而非圆括号
()
,但是在上面的
withPrintWriter
方法的调用时使用的仍然是圆括号。

  为了使得自定义控制结构和Scala提供的控制结构使用风格类似,可以将上面代码中的
()
替换成
{}


  比如,

println("Hello, world!")
println { "Hello, world!" }


  执行效果是相同的,

  


  需要注意的是,将圆括号替换成花括号只能发生在接收一个参数值的函数上,如果某个函数接收的是两个参数,那么将圆括号改成花括号就会报错,如下所示

val g = "Hello, world!"

g.substring {7, 9}
g.substring(7, 9)


  执行结果如下,

  


4、使用柯里化改造
withPrintWriter
函数

  接下来以前面的
withPrintWriter
函数为例,对其进行改造使调用时更像Scala提供的控制结构语法。回顾一下前面的
withPrintWriter
函数,参数列表中有两个参数,根据上一节中的分析,无法直接将圆括号改造成花括号。

  要怎么办?回想一下之前的柯里化,可以将一个只有一个参数列表的函数改造成有多个参数列表的函数,这里如果将
withPrintWriter
进行柯里化,把后面的函数参数分离成只有一个参数的参数列表,那么就可以将其改造成花括号了,如下所示

def withPrintWriter(file: File)(op: PrintWriter => Unit) {
val writer = new PrintWriter(file)
try {
op(writer)
} finally {
writer.close()
}
}


  然后,我们再看一下对改造后的
withPrintWriter
函数的调用方法

val file = new File("date.txt")

withPrintWriter(file) {
writer => writer.println(new java.util.Date)
}


  比较一下自定义的控制结构和Scala提供的
for
while
循环,使用风格已经很类似了。

五、By-name参数

  再仔细看一下上面的改造后的
withPrintWriter
函数,在花括号中需要传入一个
PrintWriter
类型的
writer
变量,而在
whie
for
等Scala控制结构的函数体中是不需要参数传递的。这个可以使用by-name参数来解决。

  接下来自定义一个
myAssert
函数,该函数接收一个函数参数,这个函数参数没有输入参数,直接用一个
()
来表示。然后根据一个
Boolean
类型的变量来确定是否执行该函数参数。

1、不使用by-name参数

  在这种情况下,函数定义如下

var assertionsEnabled = true

def myAssert(predicate: () => Boolean) =
if (assertionsEnabled && !predicate())
throw new AssertionError


  光从定义上看,上面这个函数定义好像也没什么。但是在使用时,

myAssert(5 > 3)
myAssert(() => 5 > 3)


  从执行结果来看,第二种写法才是正确的。

  


  这个
myAssert
函数定义后调用的话会显得很麻烦,代码臃肿。

2、使用by-name参数的情况

  接下来以by-name参数的形式重新定义一个
byNameAssert
函数,by-name参数的情况的函数参数不再以
()=>
开头,而是以
=>
开头,如下

def byNameAssert(predicate: => Boolean) =
if (assertionsEnabled && ! predicate)
throw new AssertionError


  此时再看一下该函数的调用

byNameAssert(5 > 3)


  执行结果如下

  


3、boolean类型参数函数

  如果将
=>
也省略掉,直接指定
boolAssert
函数接收一个
Boolean
类型的参数,从函数的调用形式来看,和前面的
byNameAssert
函数相同,如下所示

def boolAssert(predicate: Boolean) =
if (assertionsEnabled && !predicate)
throw new AssertionError

boolAssert(5 > 3)


  执行结果如下:

  


4、三个函数的比较

  将上面的三个方法
myAssert
byNameAssert
boolAssert
以及方法的调用写入同一个
ByName.scala
文件中,该scala文件完整内容如下:

object ByName {
var assertionsEnabled = true

def main(args: Array[String]) {
myAssert(() => 5 > 3)
byNameAssert(5 > 3)
boolAssert(5 > 3)
}

def myAssert(predicate: () => Boolean) =
if (assertionsEnabled && !predicate())
throw new AssertionError

def byNameAssert(predicate: => Boolean) = if (assertionsEnabled && ! predicate) throw new AssertionError

def boolAssert(predicate: Boolean) =
if (assertionsEnabled && !predicate)
throw new AssertionError
}


  对
ByName.scala
进行编译,查看编译后的结果。

  首先看一下三个函数的定义,其中
myAssert
byNameAssert
都是接收一个
Function0
类型的对象,而
boolAssert
接收一个
boolean
类型的参数。

  


  再看一下函数调用,
myAssert
byNameAssert
方法都是传入上面的
Function0
类型的对象,该对象的apply方法计算出
5 > 3
的结果为
true
并返回。而
boolAssert
方法,在方法调用之前就已经计算出了
5 > 3
的值为true,然后直接将该
true
传入
boolAssert
方法。

  


 总结一下,
myAssert
byNameAssert
方法在编译后的代码中不管是方法定义,还是方法调用,都是相同的,所以
byNameAssert
的确是
myAssert
的简化形式。这两种形式的方法,接收的都是一个函数参数,该函数参数的函数值会编译成一个
Function0
类型对象,并且方法调用时该对象的
apply
方法才会去计算
5 > 3
的结果。

  而
boolAssert
方法,编译后的接收参数只是一个
boolean
类型的变量,并且方法调用时,首先将
5 > 3
进行解析,然后将解析后的结果传入
boolAssert
方法。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: