Scala编程-第9章 控制抽象- 高飞网

第9章 控制抽象

2017-03-22 09:19:09.0

9.1 减少代码重复

    所有的函数都可以被分成通用部分(它们在每次函数调用中都相同),以及非通用部分(在不同的函数调用中可能会变化)。通用部分是函数体,而非通用部分必须由参数提供。当把函数参数值用做参数时,算法的非通用部分就是它代表的某些其他算法。在这种函数的每一次调用中,都可以把不同的函数值作为参数传入,于是被调用函数将在每次选用参数的时候调用传入的函数值。这种高阶函数(higher-order function)——带其他函数做参数的函数——提供了额外的机会去组织和简化代码。

    高阶函数的好处之一是它们可以创造控制抽象从而减少代码重复。

    下面以这样一个需求为例,查询当前目录下,以.scala结尾的文件列表。

object FileMatcher{
    private def filesHere = (new java.io.File(".")).listFiles
    def fileEnding(query:String)={
        for(file<-filesHere;if(file.getName.endsWith(query)))
            yield file
    }   
    def main(args:Array[String]){
        fileEnding(".scala").foreach(println)
    }   
}

    在上面的例子中,filesHere是私有的,因此只有fileEncing是提供给外部的API。

    接下来扩展一下功能,上面是查询以什么结尾的,现在加上:文件名包含指定内容的文件列表和支持正则表达式查找文件名

object FileMatcher{
    private def filesHere = (new java.io.File("../")).listFiles
    def fileEnding(query:String)={
        for(file<-filesHere;if(file.getName.endsWith(query)))
            yield file
    }   
    def fileContains(query:String)={
        for(file<-filesHere;if(file.getName.contains(query)))
            yield file
    }   
    def fileRegex(query:String)={
        for(file<-filesHere;if(file.getName.matches(query)))
            yield file
    }   
    def main(args:Array[String]){
        fileEnding(".scala").foreach(println)
        println("----------------------------")
        fileContains("hello").foreach(println)
        println("----------------------------")
        fileRegex("^a.*").foreach(println)

    }   
}

    我们发现,三个API:fileEnding、fileContains、fileRegex中,其中除了核心的查询代码,其他都是完全一样的。

   接下来用函数值作为参数,简化代码

object FileMatcher2{
    private def filesHere = (new java.io.File("../")).listFiles
    def fileMatching(query:String,matcher:(String,String)=>Boolean)={
        for(file<-filesHere;if(matcher(file.getName,query)))
            yield file
    }
    def fileEnding(query:String)={
        fileMatching(query,_.endsWith(_))
    }
    def fileContains(query:String)={
        fileMatching(query,_.contains(_))
    }
    def fileRegex(query:String)={
        fileMatching(query,_.matches(_))
    }
    def main(args:Array[String]){
        fileEnding(".scala").foreach(println)
        println("----------------------------")
        fileContains("hello").foreach(println)
        println("----------------------------")
        fileRegex("^a.*").foreach(println)

    }
}


9.2 简化客户端代码

    高阶函数的另一个重要应用是把它们放在API里使葡萄糖客户端更简洁。Scala的集合类型的特定用途循环方法提供了一个很好的例子。

普通的方式判断一个集合中是否有负数

object Exists{
    def main(args:Array[String]){
        println(containsNeg(List(1,2,3,4))) 
        println(containsNeg(List(1,2,-3,4)))    
    }   
    def containsNeg(nums:List[Int]):Boolean={
        var exists = false
        for(num<-nums)
            if(num<0)
                exists = true;
        exists
    }   
}

使用exists

object Exists2{
    def main(args:Array[String]){
        println(containsNeg(List(1,2,3,4))) 
        println(containsNeg(List(1,2,-3,4)))    
    }   
    def containsNeg(nums:List[Int])=nums.exists(_<0);
}

效果是一样的。

    exists方法代表了控制抽象。是Scala库提供的特定用途循环架构而不是像while或for那样内建在Scala语言里的。

又如,查询是否存在奇数

object Exists2{
    def main(args:Array[String]){
        println(containsNeg(List(1,2,3,4))) 
        println(containsNeg(List(1,2,-3,4)))    
        println("-------------------------")    
        println(containsNeg(List(1,-3,4)))  
        println(containsNeg(List(2,4))) 
    }   
    def containsNeg(nums:List[Int])=nums.exists(_<0)
    def containsOdd(nums:List[Int])=nums.exists(_%2==1)
}


9.3 柯里化(currying)

未柯里化的方法定义:

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

scala> plainOldSum(1,2)
res0: Int = 3

相对应的柯里化的方法定义:

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

scala> curriedSum(1)(2)
res1: Int = 3


9.4 编写新的控制结构

    在拥有头等函数的语言中,即使语言的语法是固定的,也可以有效地制作新的控制结构。如,下面是“双倍”控制结构,能重复一个操作两次并返回结果。

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

scala> twice(_+1,5)
res2: Double = 7.0

    又如,打开资源,处理资源,关闭资源的结构。

import java.io._
import java.util._
object WithPrintWriter{
    def main(args:Array[String]){
        withPrintWriter(new File("abc.txt"),
        writer=>writer.print(new Date()))
    }   
    def withPrintWriter(file:File,op:PrintWriter=>Unit){
        val print = new PrintWriter(file);
        try{
            op(print)
        }finally{
            print.close()
        }   
    }   
}

    withPrintWriter方法,能确保在文件在结尾被关闭。这个技巧被称为借贷模式(loan pattern)。因为控制抽象函数,如withPrintWriter,打开资源并“贷出”给函数。例子中的withPrintWriter把PrintWriter借给op。当函数完成的时候,它发出信号说明不再需要“借”资源,于是资源被关闭在finally块中,以确信其确实被关闭。

    在Scala中,如果函数只要一个参数,就可以把小括号替换为大括号。如:

scala> println("helloworld")
helloworld

scala> println {"helloworld"}
helloworld


虽然withPrintWriter有两个参数,无法直接使用花括号的形式,但由于op参数作为最后一个参数,因此可以将之前的file柯里化,使之变为:

import java.io._
import java.util._
object WithPrintWriter2{
    def main(args:Array[String]){
        withPrintWriter2(new File("abc.txt")){
            writer=>writer.print(new Date())
        }
    }
    def withPrintWriter2(file:File)(op:PrintWriter=>Unit){
        val print = new PrintWriter(file);
        try{
            op(print)
        }finally{
            print.close()
        }
    }
}



9.5 传名参数(by-name parameter)

    下面的例子实现一个断言架构。myAssert函数将一个函数值作为输入并参考一个标志位来决定该做什么。如果标志位被设置了,myAssert将调用传入的函数并证实其返回了true。如果标志位被关闭了,将什么都不做。

object MyAssert{
    var assertionsEnabled = true
    def myAssert(predicate:()=>Boolean)=
        if(assertionsEnabled && !predicate())
            throw new AssertionError
    def main(args:Array[String]){
        myAssert(()=>5<3)
    }
}

    改为传名参数

object MyAssert2{
    var assertionsEnabled = true
    def byNameAssert(predicate: => Boolean)=
        if(assertionsEnabled && !predicate)
            throw new AssertionError
    def main(args:Array[String]){
        byNameAssert(5<3)
    }   
}

    传名参数中,()被省略,它仅在参数中被允许。

    另外一种格式:

object MyAssert2{
    var assertionsEnabled = false
    def byNameAssert(predicate: => Boolean)=
        if(assertionsEnabled && !predicate)
            throw new AssertionError
    
    def boolAssert(predicate:Boolean)=
        if(assertionsEnabled && !predicate)
            throw new AssertionError


    def main(args:Array[String]){
        byNameAssert(5/0>2)
        boolAssert(5/0>2)
    }   
}

    两种方式有什么区别呢?传名参数,当运行时,先不运算5/0>2的值,先传入,当标志关闭时,就不运行了,因此没有出现异常。而后一种,会先运行5/0>2,会直接报异常。