函数(一)
in Scala with 0 comment

函数(一)

in Scala with 0 comment

介绍

内容

当程序越来越大时,你需要将代码细化为小的容易管理的模块。Scala 支持多种方法来细化程序代码,这些方法也为有经验的程序员已经掌握的:使用函数。

在本节实验中,我们将为你介绍类成员函数、局部函数等具有 Scala 特色的函数使用方法。

知识点

环境

适合人群

本课程难度为一般,属于初级级别课程,适合零基础或具有 Java 编程基础的用户。

开发准备

为了使用交互式 Scala 解释器,你可以在打开的终端中输入命令:

su -l hadoop #密码为 hadoop
scala

当出现 scala> 开始的命令行提示符时,就说明你已经成功进入解释器了。如下图所示。

image-1655705808705

步骤

函数的具体使用之类成员函数

和 Java 相比,Scala 提供了多种 Java 不支持的方法来定义函数,除了类成员函数外,Scala 还支持嵌套函数,函数字面量,函数变量等。

本节先介绍类或对象的成员函数。这也是最常见的定义函数的方法。例如,下面的例子定义了两个成员函数:

import scala.io.Source
object LongLines {
  def processFile(filename: String, width: Int) {
    val source = Source.fromFile(filename)
    for (line <- source.getLines())
      processLine(filename,width,line)
   }

   private def processLine(filename:String,
     width:Int, line:String){
     if(line.length > width)
       println(filename + ":" +line.trim)
   }

}

image-1655705818462

成员函数 processFile 使用两个参数,一个是文件名,另一个为字符长度,其作用是打印出文件中超过指定字符长度的所有行。它调用另外一个私有成员函数 processLine 完成实际的操作。

这个成员函数,如果作为脚本使用,可以使用如下代码:

LongLines.processFile(args(0),args(1).toInt)

可以看到,Scala 类成员函数的使用方法和其它面向对象的程序语言(如 Java)基本一致。在后面的内容中,我们将继续介绍 Scala 函数不同于 Java 的一些特性。

局部函数

上个例子中,processFile 使用了一个非常重要的设计原则——应用程序可以分解成多个小的函数,每个小的函数完成一个定义完好的功能。

使用这种程序设计风格,可以让程序中有相当数量的程序构造模块。通过这些小的构造模块的组合来完成较复杂的功能。每个小的构造模块应该足够简洁,以帮助理解。

这样带来的一个问题是:这些小的辅助函数的名称可能会影响到程序空间,你不能在同个程序中使用两个相同名称的函数,即使你定义私有函数。如果你设计函数库,你也不希望有些辅助函数被库函数的用户直接调用。

对于 Java 来说,你可以通过私有成员函数来达到目的。而 Scala 除了支持私有成员函数外,还支持局部函数(其作用域和局部变量类似)。

也就是说,你可以在函数的内部再定义函数,如同定义一个局部变量。例如,修改前面的 processFile 的例子如下:

import scala.io.Source
object LongLines {
    def processFile(filename: String, width: Int) {
        def processLine(filename:String,width:Int, line:String){
            if(line.length > width)
            println(filename + ":" +line.trim)
        }

    val source = Source.fromFile(filename)
    for (line <- source.getLines())
        processLine(filename,width,line)
    }
}

这个例子中,私有成员函数 processLine 移动到 processFile 内部,成为了一个局部函数。也正因为如此,processLine 可以直接访问到 processFile 的参数 filenamewidth。因此,代码可以进一步优化如下:

import scala.io.Source
object LongLines {
    def processFile(filename: String, width: Int) {
        def processLine(line:String){
            if(line.length > width)
            println(filename + ":" +line.trim)
    }

    val source = Source.fromFile(filename)
    for (line <- source.getLines())
        processLine(line)
    }
}

代码变得更简洁了,对不对?局部函数的作用域和局部变量作用域一样,局部函数访问包含该函数的参数是非常常见的一种嵌套函数的用法。

头等公民

Scala 中,函数是头等公民。你不仅可以定义一个函数然后调用它,你还可以写一个未命名的函数字面量,然后把它当成一个值,传递到其它函数或是赋值给其它变量。

下面的例子为一个简单的函数字面量(参考整数字面量,3 为一整数字面量)。

(x :Int ) => x +1

这是个函数字面量,它的功能为 +1。符号 => 表示这个函数将符号左边的东西(本例为一个整数),转换成符号右边的东西(加 1)。

函数字面量为一个对象(就像 3 是个对象)。因此,如果你愿意的话,可以把这个函数字面量保持在一个变量中。这个变量也是一个函数,因此你可以使用函数风格来调用它,比如:

scala> var increase = (x :Int ) => x +1
increase: Int => Int = <function1>

scala> increase(10)
res0: Int = 11

注意,函数字面量 (x:Int) => x + 1 在 Scala 内部,表示为带有一个参数的类 function1 的一个对象。其它情况比如 functionN 代表带有 N 个参数的函数,function0 代表不含参数的函数类型。

如果函数定义需要多条语句,可以使用 {},比如:

scala> var increase = (x :Int ) => {
     |    println("We")
     |    println("are")
     |    println("here")
     |    x + 1
     |    }
increase: Int => Int = <function1>

scala> increase (10)
We
are
here
res0: Int = 11

从上面的内容中,我们了解到了函数字面量的基本概念。它可以作为参数传递给其它函数,比如很多 Scala 的库允许你使用函数作为参数(比如 foreach 方法,它使用一个函数参数,为集合中每个运算调用传入的函数)。例如:

scala> val someNumbers = List ( -11, -10, - 5, 0, 5, 10)
someNumbers: List[Int] = List(-11, -10, -5, 0, 5, 10)

scala> someNumbers.foreach((x:Int) => println(x))
-11
-10
-5
0
5
10

再比如,Scala 的集合也支持一个 filter 方法用来过滤集合中的元素,filter 的参数也是一个函数,比如:

scala> someNumbers.filter( x => x >0)
res1: List[Int] = List(5, 10)

使用 x => x >0,过滤掉小于 0 的元素。如果你熟悉 lambda表达式, x => x >0 为函数的 lambda 表达式。

函数字面量的一些简化写法

Scala 提供了多种方法来简化函数字面量中多余的部分。比如前面例子中,filter 方法中使用的函数字面量,完整的写法如下:

 (x :Int ) => x +1

首先可以省略掉参数的类型,Scala 可以根据上下文推算出参数的类型,函数定义可以简化为:

 (x) => x +1

这个函数可以进一步去掉参数的括号,这里的括号不起什么作用:

 x => x +1

Scala 还可以进一步简化:Scala 允许使用“占位符”下划线 “_” 来替代一个或多个参数,只要这个参数值函数定义中只出现一次,Scala 编译器可以推断出参数。比如:

scala> val someNumbers = List ( -11, -10, - 5, 0, 5, 10)
someNumbers: List[Int] = List(-11, -10, -5, 0, 5, 10)

scala> someNumbers.filter(_ >0)
res0: List[Int] = List(5, 10)

可以看到,简化后的函数定义为 _ > 0,你可以这样来理解:就像我们以前做过的填空题,“_” 为要填的空,Scala 会来完成这个填空题,而你来定义填空题。

有时,如果你使用 _ 来定义函数,可能没有提供足够的信息给 Scala 编译器。此时, Scala 编译器将会报错。比如,定义一个加法函数如下:

scala> val f = _ + _
<console>:7: error: missing parameter type for expanded function ((x$1, x$2) => x$1.$plus(x$2))
       val f = _ + _
               ^
<console>:7: error: missing parameter type for expanded function ((x$1: <error>, x$2) => x$1.$plus(x$2))
       val f = _ + _

Scala 编译器无法推断出 _ 的参数类型,所以报错了。但如果你给出了参数的类型,就依然可以使用 _ 来定义函数。比如:

scala> val f = (_ :Int ) + ( _ :Int)
f: (Int, Int) => Int = <function2>

scala> f (5,10)
res1: Int = 15

因为 _ 替代的参数在函数体中只能出现一次,因此多个 “_” 代表多个参数。第一个 “_” 代表第一个参数,第二个 “_” 代表第二个参数,以此类推。

部分应用的函数

前面的例子中,我们使用了 “_” 来代替单个的参数。实际上,你也可以使用 “_” 来代替整个参数列表。比如说,你可以使用 println _ 来代替 println (_)

someNumbers.foreach(println _)

Scala 编译器自动将上面代码解释成:

someNumbers.foreach( x => println (x))

因此这里的 “_” 代表了 println 的整个参数列表,而不仅仅替代单个参数。

当你采用这种方法使用 “_”,你就创建了一个部分应用的函数(partially applied function)。在 Scala 中,当你调用函数,传入所需参数,你就把函数“应用”到参数。比如:一个加法函数。

scala> def sum = (_:Int) + (_ :Int) + (_ :Int)
sum: (Int, Int, Int) => Int

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

一个部分应用的函数指的是你在调用函数时,不指定函数所需的所有参数。这样,你就创建了一个新的函数,这个新的函数就称为原始函数的部分应用函数。比如说,我们固定 sum 的第一和第三个参数,定义如下的部分应用函数:

scala> val b = sum ( 1 , _ :Int, 3)
b: Int => Int = <function1>

scala> b(2)
res1: Int = 6

变量 b 的类型为一函数,具体类型为 function1(带一个参数的函数),它是由 sum 应用了第一个和第三个参数构成的。

调用 b(2),实际上调用 sum(1, 2, 3)

再比如,我们定义一个新的部分应用函数,只固定中间参数:

scala> val c = sum (_:Int, 2, _:Int)
c: (Int, Int) => Int = <function2>

scala> c(1,3)
res2: Int = 6

变量 c 的类型为 function2,调用 c(1, 3) 实际上也是调用 sum(1, 2, 3)

在 Scala 中,如果你定义一个部分应用函数并且能省去所有参数。比如 println _,你也可以省掉 “_” 本身。比如:

someNumbers.foreach(println _)

可以写成:

someNumbers.foreach(println)

闭包

到目前为止,我们介绍的函数都只引用到传入的参数。假如我们定义如下的函数:

(x:Int) => x + more

这里我们引入一个自由变量 more。它不是所定义函数的参数,而这个变量定义在函数外面。比如:

var more = 1

那么我们有如下的结果:

scala> var more = 1
more: Int = 1

scala> val addMore = (x:Int) => x + more
addMore: Int => Int = <function1>

scala> addMore (100)
res1: Int = 101

这样定义的函数变量 addMore 成为一个“闭包”。因为它引用到函数外面定义的变量。定义这个函数的过程,是将这个自由变量捕获而构成一个封闭的函数。有意思的是,当这个自由变量发生变化时,Scala 的闭包能够捕获到这个变化,因此 Scala 的闭包捕获的是变量本身而不是当时变量的值。

比如:

scala> more =  9999
more: Int = 9999

scala> addMore (10)
res2: Int = 10009

同样的,如果变量在闭包中发生变化,也会反映到函数外面定义的闭包的值。比如:

scala> val someNumbers = List ( -11, -10, -5, 0, 5, 10)
someNumbers: List[Int] = List(-11, -10, -5, 0, 5, 10)

scala> var sum =0
sum: Int = 0

scala> someNumbers.foreach ( sum += _)

scala> sum
res4: Int = -11

可以看到,在闭包中修改 sum 的值,其结果还是传递到闭包的外面。

如果一个闭包所访问的变量有几个不同的版本,比如一个闭包使用了一个函数的局部变量(参数),然后这个函数调用很多次,那么所定义的闭包应该使用所引用的局部变量的哪个版本呢?

简单的说,该闭包定义所引用的变量为定义该闭包时变量的值,也就是定义闭包时相当于保存了当时程序状态的一个快照。比如,我们定义下面一个函数闭包:

scala> def makeIncreaser(more:Int) = (x:Int) => x + more
makeIncreaser: (more: Int)Int => Int

scala> val inc1=makeIncreaser(1)
inc1: Int => Int = <function1>

scala> val inc9999=makeIncreaser(9999)
inc9999: Int => Int = <function1>

scala> inc1(10)
res5: Int = 11

scala> inc9999(10)
res6: Int = 10009

当你调用 makeIncreaser(1) 时,你就创建了一个闭包,该闭包定义的 more 的值为 1,而调用 makeIncreaser(9999) 所创建的闭包的 more 的值为 9999。此后你也无法修改已经返回的闭包的 more 的值。因此 inc1 始终为加一,而 inc9999 始终为加 9999

可变参数、命名参数和缺省参数

前面我们介绍的函数的参数是固定的,本节介绍 Scala 函数支持的可变参数列表、命名参数和参数缺省值定义。

重复参数

Scala 在定义函数时,允许指定最后一个参数重复(变长参数),从而允许函数调用者使用变长参数列表来调用该函数。Scala 中使用 “*” 来指明该参数为重复参数。例如:

scala> def echo (args: String *) =
     |   for (arg <- args) println(arg)
echo: (args: String*)Unit

scala> echo()

scala> echo ("One")
One

scala> echo ("Hello","World")
Hello
World

在函数内部,变长参数的类型实际上是一个数组。比如上例的 String * 类型实际为 Array[String]。然而,如果你现在试图直接传入一个数组类型的参数给这个参数,编译器会报错:

scala> val arr = Array("What's","up","doc?")
arr: Array[String] = Array(What's, up, doc?)

scala> echo (arr)
<console>:10: error: type mismatch;
 found   : Array[String]
 required: String
              echo (arr)
                    ^

为了避免这种情况发生,你可以通过在变量后面添加 _* 来解决。这个符号告诉 Scala 编译器,在传递参数时,逐个传入数组的每个元素,而不是数组整体。

scala> echo (arr: _*)
What's
up
doc?

命名参数

通常情况下,调用函数时,参数传入和函数定义时参数列表是一一对应的。

scala> def  speed(distance: Float, time:Float) :Float = distance/time
speed: (distance: Float, time: Float)Float

scala> speed(100,10)
res0: Float = 10.0

使用命名参数时,允许你使用任意顺序传入参数。比如下面的调用:

scala> speed( time = 0,distance = 100)
res1: Float = 10.0

scala> speed(distance = 100,time = 10)
res2: Float = 10.0

缺省参数值

Scala 在定义函数时,允许指定参数的缺省值,从而允许在调用函数时不指明该参数,此时该参数使用缺省值。缺省参数通常配合命名参数使用,例如:

scala> def printTime(out:java.io.PrintStream = Console.out, divisor:Int =1 ) =
     | out.println("time = " + System.currentTimeMillis()/divisor)

printTime: (out: java.io.PrintStream, divisor: Int)Unit

scala> printTime()
time = 1383220409463

scala> printTime(divisor = 1000)
time = 1383220422