第08章 函数和闭包

第08章 函数和闭包

1 方法
object LongLines {
  def main(args: Array[String]): Unit = {
    val width = args(0).toInt
    for (arg <- args.drop(1))
      processFile(arg, width)
  }

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

  private def processLine(filename: String, width: Int, line: String): Unit = {
    if (line.length > width)
      println(filename + ":" + line.trim)
  }
}
  • 定义函数最常用的方式是作为某个对象的成员,这样的函数称为方法
2 局部函数
object LongLines {
  def main(args: Array[String]): Unit = {
    val width = args(0).toInt
    for (arg <- args.drop(1))
      processFile(arg, width)
  }

  def processFile(filename: String, width: Int): Unit = {

    def processLine(line: String): Unit = {
      if (line.length > width)
        println(filename + ":" + line.trim)
    }

    val source = scala.io.Source.fromFile(filename);
    for {
      line <- source.getLines()
    } processLine(line)
  }
}
  • 函数式编程风格的一个设计原则:程序应该被分解成许多小函数,每个函数都只做明确定义的任务。单个函数通常都很小,这种风格的好处是可以让程序员灵活的将许多构建单元组装起来,完成更复杂的任务。这种方式的一个问题是助手函数的名称会污染整个程序的命名空间。一旦函数被打包进可复用的类和对象当中,我们通常希望使用者不要直接看到这些函数。在 java 可以将方法定义为 private,这种方式同样适用于 scala。不过 scala 提供了另一种思路:在某个函数内部定义函数,就像局部变量一样
  • 局部函数移除 private 修饰符,局部函数可以访问包含他们的函数的参数
3 一等函数
val increment = (x: Int) => x + 1
val value = increment(10)
println(value)
  • scala 支持一等函数,不仅可以定义函数并调用它们,还可以用匿名的字面量来编写函数并将它们作为值进行传递
  • 函数字面量被编译为类,并在运行时实例化函数值,因此函数字面量存在于源码中,函数值以对象的形式存在于运行时,函数值是对象,所以可以将它们存放在变量中,它们同时也是函数,所以可以用常规的圆括号来调用它们。
  • 如果函数字面包含多条语句,可以将函数体用花括号括起来
4 函数字面量的简写形式
val someNumbers = List(-11, -10, 5)
someNumbers.filter((x) => x > 0)
someNumbers.filter(x => x > 0)
  • 略去参数类型声明
  • 省去某个类型推断的参数两侧圆括号
5 占位符语法
val someNumbers = List(-11, -10, 5)
//等价于 x => x > 0
someNumbers.filter(_ > 0)
//给出类型
val f = (_:Int) + (_:Int)
  • 用下划线作为占位符,用来表示一个或多个参数,只要满足每个参数只在函数字面量中出现一次即可。
  • 有时候当你用下划线作为参数占位符时,编译器可能并没有足够多的信息来推断缺少的参数类型,例如,假定你只写了 _ + _ ,这类情况下,你可以用冒号来给出类型
  • 多个下划线意味着多个参数,而不是对单个参数的重复使用,第一个下划线代表第一个参数,第二个下划线代表第二个参数,依次类推
6 部分应用函数
val someNumbers = List(-11, -10, 5)
someNumbers.foreach(println _)
someNumbers.foreach(println)
def sum (a: Int, b: Int, c: Int) = a + b + c
sum(1, 2, 3)
//val sum = (a: Int, b: Int, c: Int) => a + b + c
//Error:(7, 13) _ must follow method; cannot follow (Int, Int, Int) => Int
//上面方式无法使用,sum为函数字面量,无法使用 val a = sum _
val a = sum _
a(1, 2, 3)
a.apply(1,2,3)
//functin1, 非 function3
val b = sum (1, _:Int, 3)
b(3)
  • 上面的下划线是整个参数列表的占位符,而非单个参数的占位符
  • println 函数名后面要加空格,否则编写会认为你引用了一个名为 println_方法
  • 当你用下划线作为整个参数列表的占位符时,实际上是在编写一个部分应用函数
  • 在scala中,当你调用某个函数,传入任何参数时,你实际上是应用那个函数到这些参数上
  • 部分应用函数是一个表达式,在这个表达式中,并不给出函数需要的所有参数,而是给出部分,或完全不给,val a = sum _ 背后发生的事情是:名为 a 的变量指向了一个函数值对象,这个函数值是一个从scala编译器自动从sum _ 这个部分应用函数表达式生成的类的实例。由编译器生成的这个类有一个接收3个参数的apply方法,val b = sum (1, _:Int, 3) ,由于只有一个参数缺失,编译器会生成一个新的函数类,这个类的apply方法接收一个参数
  • 这类用下划线表示整个参数列表的表达式,这是一种将 def 方法变成函数值的方式
  • 如果你要的部分应用函数表达式并不给出任何参数,比如 println _ 或 sum _,可以在需要这样的一个函数的地方更加精简,连下划线也不用写,如 someNumbers.foreach(println),这种形式只有明确需要函数的地方才被允许,比如 someNumbers.foreach 调用。编译器知道这里需要的是一个函数
7 闭包
//more 非参数,自由变量
var more = 1
val addMore = (x :Int) => x + more
addMore 10

def makeIncreaser(more: Int) = {
    (x: Int) => x + more
}
  • more 是一个自由变量,非函数入参,运行时从这个函数字面量创建出来的函数值(对象)被称为闭包,该名称源于"捕获"其自由变量从而“闭包”该函数字面量的动作
  • scala 的闭包捕获的是变量本身,而不是变量引用的值,闭包对捕获的变量的修改也能在闭包外看到
8 特殊的函数调用形式
可变参数
def echo(args: String*) = {
    for (arg <- args) println(arg)
}
val arr = Array("What's", "up", "doc?")
//echo(arr)编译会报错
echo(arr: _*)
  • 在参数的类型之后加上一个星号( * ),可变参数的类型是一个所声明的参数类型的Array
  • 在数组实参的后面加上冒号和一个 _*符号,这种表示法告诉编译器将arr的每个元素作为参数传给 echo,而不是将所有元素放在一起作为单个实参传入
带名字的参数
def speed(distance: Float, time: Float) = distance / time
speed(time = 10, distance = 100)
speed(100, 10)

带名字的参数让你可以用不同的顺序将参数传给函数,其语法是简单的在每个实参加上参数名和等号,还可以按位置和带名字的参数,这种情况下,按位置的参数需要放在前面

缺省参数值
def printTime(out: java.io.PrintStream = System.out) = {
    out.println("time:" + System.currentTimeMillis())
}

def printTime2(out: java.io.PrintStream = Console.out, divisor: Int = 1): Unit = {
    out.println("time:" + System.currentTimeMillis() / divisor)
}
printTime();
printTime2(divisor = 1000)

scala 允许你给函数参数指定缺省值,这些缺省值的参数可以不出现在函数调用中,对应的参数将会被填充为缺省值,缺省参数通常和命名参数一起使用

9 尾递归

scala编译器能够检查到尾递归并将它替换成跳转到函数的最开始,并在跳转之前将参数更新为新的值,通常递归算法比基于循环的算法更优雅、精简。如果解决方案是尾递归,那么我们不需要额外的允许开销

跟踪尾递归函数
def boom(x: Int): Int = if (x == 0) throw new Exception("boom!") else boom(x - 1) + 1
  • 尾递归函数并不会再每次调用时构建一个新的栈帧,所有的调用都会在同一个栈帧中执行
  • boom 函数非尾递归,因为它在递归调用之后还执行了一个递增操作
尾递归的局限
//间接尾递归,无法优化
def isEven(x: Int): Boolean = if (x == 0) true else isOld(x - 1)
def isOld(x: Int): Boolean = if (x == 0) false else isEven(x - 1)
  • 在scala中使用尾递归是比较受限的,因为用 JVM指令集实现更高级形式的尾递归非常困难。scala 只能对那些直接尾递归调用自己的函数做优化。如果递归调用是间接的,scala 就没法优化它们
  • 尾递归优化仅适用于某个方法或嵌套函数在最后一步操作中直接调用自己
10 结语
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值