【Scala之旅】高阶函数

本节翻译自

综述:Scala混合了面向对象和函数式的特性。在函数式编程语言中,函数是“头等公民”,可以像任何其他数据类型一样被传递和操作。在本节中,我们将会看到如何通过那些使用或返回函数的函数来提高我们的工作效率。

类型推断

Scala 编译器通常可以推断出表达式的类型,因此你不必显式声明它。

省略类型

val businessName = "Montreux Jazz Café"

编译器可以检测到 businessName 是一个 String。它的工作原理与方法类似:

def squareOf(x: Int) = x * x

编译器可以推断返回类型是一个 Int,所以不需要显式的返回类型。

对于递归方法,编译器不能推断出结果类型。由于这个原因,下面的程序会使编译器失败:

def fac(n: Int) = if (n == 0) 1 else n * fac(n - 1)

多态方法被调用或者泛型类被实例化时,也不用强制地指定类型参数。Scala 编译器会从上下文和实际方法/构造函数参数的类型中推断出这些缺失的类型参数。

这里有两个例子:

case class MyPair[A, B](x: A, y: B);
val p = MyPair(1, "scala") // type: MyPair[Int, String]

def id[T](x: T) = x
val q = id(1)              // type: Int

编译器使用 MyPair 的参数类型来确定 AB 的类型。类似于 x 的类型。

参数

编译器从不推断方法参数类型。但是,在某些情况下,它可以在函数作为参数传递时推断匿名函数参数类型。

Seq(1, 3, 4).map(x => x * 2)  // List(2, 6, 8)

map 的参数是 f: A => B。因为我们把整数放在 Seq 中,编译器知道 AInt(即 x 是一个整数)。因此,编译器可以从 x * 2 推断出 BInt 类型。

何时不依靠类型推断

通常认为在公共 API 中声明成员的类型更具可读性。因此,我们建议你对任何将暴露给用户的代码 API 进行明确的类型标记。

此外,类型推断有时可能推断出一个太特定的类型。 假设我们写:

var obj = null

我们现在不能继续进行重新分配:

obj = new AnyRef

它不会编译,因为 obj 推断的类型是 Null。由于该类型的唯一值为空,因此不可能分配不同的值。

高阶函数

Scala 允许对高阶函数的定义。这些函数将其他函数作为参数,或者其结果是一个函数。这是可能的,因为函数在 Scala 中是一等公民。在这一点上,术语可能会有些混乱,我们使用短语“高阶函数”来表示++将函数作为参数++或++返回函数++的方法和函数。

最常见的例子之一是可用于Scala中的集合的高阶函数 map

val salaries = Seq(20000, 70000, 40000)
val doubleSalary = (x: Int) => x * 2
val newSalaries = salaries.map(doubleSalary) // List(40000, 140000, 80000)

doubleSalary 是一个输入一个 Int x 返回 x * 2 的函数。通常,箭头左侧的元组 => 是一个参数列表,右侧表达式的值是返回值。在第三行,函数 doubleSalary 被应用于工资列表中的每个元素。

为了简化代码,我们可以使函数匿名并直接将其作为参数传递给 map:

val salaries = Seq(20000, 70000, 40000)
val newSalaries = salaries.map(x => x * 2) // List(40000, 140000, 80000)

注意 x 在上面的例子中没有被声明为 Int。这是因为编译器可以根据函数映射的类型推断出类型。下面是以一种更习惯的方法写的同一段代码:

val salaries = Seq(20000, 70000, 40000)
val newSalaries = salaries.map(_ * 2)

由于Scala编译器已经知道参数的类型(一个 Int),因此你只需提供该函数的右侧。唯一需要注意的是,你需要使用_代替参数名称(在前面的例子中是x)。

强迫方法转化为函数

也可以将方法作为参数传递给高阶函数,因为 Scala 编译器会将该方法强制为一个函数。

case class WeeklyWeatherForecast(temperatures: Seq[Double]) {

  private def convertCtoF(temp: Double) = temp * 1.8 + 32

  def forecastInFahrenheit: Seq[Double] = temperatures.map(convertCtoF) // <-- passing the method convertCtoF
}

这里 convertCtoF 方法被传递给 forecastInFahrenheit。这是可能的,因为编译器将 convertCtoF 强制转换为函数 x => convertCtoF(x)(注意: x 将是一个生成的名称,在其范围内保证是唯一的)。

接受函数的函数

使用高阶函数的一个原因是减少冗余代码。假设你想要一些可以通过各种因素提高某人薪水的方法。不创建更高阶的函数,它可能看起来像这样:

object SalaryRaiser {

  def smallPromotion(salaries: List[Double]): List[Double] =
    salaries.map(salary => salary * 1.1)

  def greatPromotion(salaries: List[Double]): List[Double] =
    salaries.map(salary => salary * math.log(salary))

  def hugePromotion(salaries: List[Double]): List[Double] =
    salaries.map(salary => salary * salary)
}

请注意,三种方法中的每一种方法仅以乘法因数变化。为了简化,您可以将重复的代码提取到更高阶的函数中,如下所示:

object SalaryRaiser {

  private def promotion(salaries: List[Double], promotionFunction: Double => Double): List[Double] =
    salaries.map(promotionFunction)

  def smallPromotion(salaries: List[Double]): List[Double] =
    promotion(salaries, salary => salary * 1.1)

  def bigPromotion(salaries: List[Double]): List[Double] =
    promotion(salaries, salary => salary * math.log(salary))

  def hugePromotion(salaries: List[Double]): List[Double] =
    promotion(salaries, salary => salary * salary)
}

新方法 promotion 接受类型 Double => Double 的加薪函数作为参数(即输入 Double 并返回 Double 的函数),然后返回乘积。

返回函数的函数

有些情况下你想要生成一个函数。这是一个返回函数的方法的例子。

def urlBuilder(ssl: Boolean, domainName: String): (String, String) => String = {
  val schema = if (ssl) "https://" else "http://"
  (endpoint: String, query: String) => s"$schema$domainName/$endpoint?$query"
}

val domainName = "www.example.com"
def getURL = urlBuilder(ssl=true, domainName)
val endpoint = "users"
val query = "id=1"
val url = getURL(endpoint, query) // "https://www.example.com/users?id=1": String

注意 urlBuilder (String, String) => String 的返回类型。这意味着返回的匿名函数需要两个字符串并返回一个字符串。在这个例子里,返回的匿名函数是 (endpoint: String, query: String) => s"https://www.example.com/$endpoint?$query"

嵌套方法

在Scala中,方法可以嵌套方法。下面的对象提供了计算一个给定数字的阶乘的 factorial 方法:

def factorial(x: Int): Int = {
    def fact(x: Int, accumulator: Int): Int = {
    if (x <= 1) accumulator
        else fact(x - 1, x * accumulator)
    }  
    fact(x, 1)
}

println("Factorial of 2: " + factorial(2))
println("Factorial of 3: " + factorial(3))

这个程序的输出是:

Factorial of 2: 2
Factorial of 3: 6

柯里化

方法可以定义多个参数列表。当使用较少数量的参数列表调用某个方法时,这将产生一个将缺少的参数列表作为其参数的函数 这种形式上被称为柯里化

下面是一个例子,定义在 Scala 集合的 Traversable 特质中:

def foldLeft[B](z: B)(op: (B, A) => B): B

foldLeft 将二元运算符 op 应用于初始值 z 以及此可遍历的所有元素,从左到右。下面显示的是它的用法示例。

从初始值 0 开始,foldLeft 在这里将函数 (m, n) => m + n 应用于 List 中的每个元素和先前的累加值。

val numbers = List(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)
val res = numbers.foldLeft(0)((m, n) => m + n)
print(res) // 55

多个参数列表具有更详细的调用语法;因此应该谨慎使用。建议的使用案例包括:

单函数参数

在单个函数参数的情况下,如上面 foldLeftop 所示,多个参数列表允许使用简洁的语法将匿名函数传递给方法。没有多个参数列表,代码将如下所示:

numbers.foldLeft(0, {(m: Int, n: Int) => m + n})

请注意,此处使用多个参数列表使我们能够利用 Scala 类型推断来使代码更加简洁,如下所示;这在一个没有柯里化定义的函数中是不可能的。

numbers.foldLeft(0)(_ + _)

另外,它允许我们修改参数 z 并传递一个部分函数并重用它,如下所示:

val numbers = List(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)
val numberFunc = numbers.foldLeft(List[Int]())_

val squares = numberFunc((xs, x) => xs:+ x*x)
print(squares.toString()) // List(1, 4, 9, 16, 25, 36, 49, 64, 81, 100)

val cubes = numberFunc((xs, x) => xs:+ x*x*x)
print(cubes.toString())  // List(1, 8, 27, 64, 125, 216, 343, 512, 729, 1000)

隐式参数

要将参数列表中的某些参数指定为 implicit,应使用多个参数列表。一个例子是:

def execute(arg: Int)(implicit ec: ExecutionContext) = ???

换名参数

换名参数 只在使用时进行求值。它们与换值参数 形成对比。要创建一个换名参数,只需将 => 添加到其类型前。

def calculate(input: => Int) = input * 37

换名参数具有一个优点:如果不在函数体中使用则不求值的优点。另一方面,换值参数的优点是它们只被求值一次。

下面是我们如何实现 while 循环的一个示例:

def whileLoop(condition: => Boolean)(body: => Unit): Unit =
  if (condition) {
    body
    whileLoop(condition)(body)
  }

var i = 2

whileLoop (i > 0) {
  println(i)
  i -= 1
}  // prints 2 1

方法 whileLoop 使用多个参数列表来获取一个条件和一个循环体。如果 conditiontrue,则执行 body,然后执行了一个递归调用。如果 conditionfalse,则 body 从未被求值过,因为我们在 => 前面加了 body 类型。

现在,当我们通过 i > 0 作为我们的 conditionprintln(i)i -= 1 作为 body,它的表现就像许多语言中的标准 while 循环。

如果这个参数是计算密集型的,或者需要一个长时间运行的代码块,比如获取URL,那么在使用参数之前,延迟对参数进行求值的能力可以帮助提高性能。

相关推荐