Scala-和-Spark-大数据分析-二-

54 阅读1小时+

Scala 和 Spark 大数据分析(二)

原文:annas-archive.org/md5/39eecc62e023387ee8c22ca10d1a221a

译者:飞龙

协议:CC BY-NC-SA 4.0

第三章:函数式编程概念

“面向对象编程通过封装移动部分使代码易于理解。函数式编程通过最小化移动部分使代码易于理解。”

  • 迈克尔·费瑟斯

使用 Scala 和 Spark 是学习大数据分析的一个非常好的组合。然而,在面向对象编程(OOP)范式的基础上,我们还需要了解为什么函数式编程(FP)概念对于编写 Spark 应用程序、最终分析数据非常重要。如前几章所述,Scala 支持两种编程范式:面向对象编程范式和函数式编程概念。在第二章,面向对象的 Scala中,我们探讨了面向对象编程范式,学习了如何在蓝图(类)中表示现实世界中的对象,并将其实例化为具有实际内存表示的对象。

在本章中,我们将重点讨论第二种范式(即函数式编程)。我们将了解什么是函数式编程,Scala 是如何支持它的,为什么它如此重要,以及使用这一概念的相关优势。更具体地说,我们将学习几个主题,例如为什么 Scala 是数据科学家的强大工具,为什么学习 Spark 范式很重要,纯函数和高阶函数HOFs)。本章还将展示一个使用 HOF 的实际案例。然后,我们将学习如何在 Scala 的标准库中处理集合之外的高阶函数中的异常。最后,我们将了解函数式 Scala 如何影响对象的可变性。

简而言之,本章将涵盖以下主题:

  • 函数式编程简介

  • 数据科学家的函数式 Scala

  • 为什么函数式编程和 Scala 对学习 Spark 至关重要?

  • 纯函数与高阶函数

  • 使用高阶函数:一个实际的使用案例

  • 函数式 Scala 中的错误处理

  • 函数式编程与数据的可变性

函数式编程简介

在计算机科学中,函数式编程(FP)是一种编程范式,它是一种构建计算机程序结构和元素的独特风格。这种独特性有助于将计算视为数学函数的求值,避免了状态变化和可变数据。因此,通过使用 FP 概念,你可以学习如何以确保数据不可变的方式编写代码。换句话说,FP 是编写纯函数的编程方法,是尽可能去除隐式输入和输出,使得我们的代码尽可能只是描述输入与输出之间的关系。

这并不是一个新概念,但λ演算(Lambda Calculus),它为函数式编程提供了基础,最早是在 1930 年代提出的。然而,在编程语言的领域中,函数式编程这个术语指的是一种新的声明式编程范式,意味着编程可以借助控制、声明或表达式来完成,而不是像传统编程语言(例如 C)中常用的经典语句。

函数式编程的优势

在函数式编程范式中,有一些令人兴奋和酷的特性,比如组合管道高阶函数,它们有助于避免编写不符合函数式编程的代码。或者,至少后来能帮助将不符合函数式的程序转换成一种面向命令式的函数式风格。最后,现在让我们从计算机科学的角度来看函数式编程的定义。函数式编程是计算机科学中的一个常见概念,其中计算和程序的构建结构被视为在评估支持不可变数据并避免状态变化的数学函数。在函数式编程中,每个函数对于相同的输入参数值都有相同的映射或输出。

随着复杂软件需求的增加,我们也需要良好结构的程序和不难编写且可调试的软件。我们还需要编写可扩展的代码,这样可以在未来节省编程成本,并且能为代码的编写和调试带来便利;甚至需要更多模块化的软件,这种软件易于扩展并且编程工作量更小。由于函数式编程的模块化特性,函数式编程被认为是软件开发中的一大优势。

在函数式编程中,其结构中有一个基本构建块,叫做无副作用的函数(或者至少是副作用非常少的函数),大多数代码都遵循这一原则。没有副作用时,求值顺序真的不重要。关于编程语言的视角,有一些方法可以强制特定的求值顺序。在某些函数式编程语言中(例如,像 Scheme 这样的贪心语言),它们对参数没有求值顺序的限制,你可以像下面这样将这些表达式嵌套在自己的 lambda 表达式中:

((lambda (val1) 
  ((lambda (val2) 
    ((lambda (val3) (/ (* val1 val2) val3)) 
      expression3)) ; evaluated third
      expression2))   ; evaluated second
    expression1)      ; evaluated first

在函数式编程中,编写数学函数时,执行顺序无关紧要,通常能使代码更具可读性。有时,人们会争辩说,我们也需要有副作用的函数。事实上,这是大多数函数式编程语言的一个主要缺点,因为通常很难编写不需要任何 I/O 的函数;另一方面,这些需要 I/O 的函数在函数式编程中实现起来也很困难。从图 1中可以看出,Scala 也是一种混合语言,通过融合命令式语言(如 Java)和函数式语言(如 Lisp)的特性演变而来。

但幸运的是,在这里我们处理的是一种混合语言,允许面向对象和函数式编程范式,因此编写需要 I/O 的函数变得相当容易。函数式编程相较于基础编程也有重大优势,比如理解和缓存。

函数式编程的一个主要优势是简洁,因为在函数式编程中,你可以编写更紧凑、更简洁的代码。此外,并发性被认为是一个主要优势,它在函数式编程中更容易实现。因此,像 Scala 这样的函数式语言提供了许多其他功能和工具,鼓励程序员做出整个范式转变,转向更数学化的思维方式。

图 1: 展示了使用函数式编程概念的概念视图

通过将焦点缩小到只有少数几个可组合的抽象概念,例如函数、函数组合和抽象代数,函数式编程概念提供了比其他范式更多的优势。例如:

  • 更接近数学思维: 你倾向于以接近数学定义的格式表达你的想法,而不是通过迭代程序。

  • 无(或至少更少)副作用: 你的函数不会影响其他函数,这对并发性和并行化非常有利,同时也有助于调试。

  • 减少代码行数而不牺牲概念的清晰度: Lisp 比非函数式语言更强大。虽然确实需要在项目中花费更多的时间思考而不是编写代码,但最终你可能会发现你变得更高效。

由于这些令人兴奋的特性,函数式编程具有显著的表达能力。例如,机器学习算法可能需要数百行命令式代码来实现,而它们可以仅通过少数几个方程式来定义。

数据科学家的函数式 Scala

对于进行交互式数据清理、处理、变换和分析,许多数据科学家使用 R 或 Python 作为他们最喜欢的工具。然而,也有许多数据科学家倾向于非常依赖他们最喜欢的工具——即 Python 或 R,并试图用这个工具解决所有的数据分析问题。因此,在大多数情况下,向他们介绍一种新工具可能是非常具有挑战性的,因为新工具有更多的语法和一套新的模式需要学习,然后才能用新工具解决他们的目的。

Spark 中还有其他用 Python 和 R 编写的 API,例如 PySpark 和 SparkR,分别允许你从 Python 或 R 中使用它们。然而,大多数 Spark 的书籍和在线示例都是用 Scala 编写的。可以说,我们认为学习如何使用 Spark 并使用与 Spark 代码编写相同语言的方式,将为你作为数据科学家提供比 Java、Python 或 R 更多的优势:

  • 提供更好的性能并去除数据处理的开销

  • 提供访问 Spark 最新和最强大功能的能力

  • 有助于以透明的方式理解 Spark 的哲学

数据分析意味着你正在编写 Scala 代码,使用 Spark 及其 API(如 SparkR、SparkSQL、Spark Streaming、Spark MLlib 和 Spark GraphX)从集群中提取数据。或者,你正在使用 Scala 开发一个 Spark 应用程序,在你自己的机器上本地处理数据。在这两种情况下,Scala 都是你真正的伙伴,并且能在时间上为你带来回报。

为什么选择函数式编程和 Scala 来学习 Spark?

在本节中,我们将讨论为什么要学习 Spark 来解决我们的数据分析问题。接着,我们将讨论为什么 Scala 中的函数式编程概念对于数据科学家来说尤其重要,它可以使数据分析变得更加简单。我们还将讨论 Spark 的编程模型及其生态系统,帮助大家更清楚地理解。

为什么选择 Spark?

Spark 是一个极速的集群计算框架,主要设计用于快速计算。Spark 基于 Hadoop 的 MapReduce 模型,并在更多形式和类型的计算中使用 MapReduce,例如交互式查询和流处理。Spark 的主要特性之一是内存计算,它帮助提高应用程序的性能和处理速度。Spark 支持广泛的应用程序和工作负载,如下所示:

  • 基于批处理的应用

  • 迭代算法,在以前无法快速运行的情况下

  • 交互式查询和流处理

此外,学习 Spark 并将其应用于你的程序并不需要花费太多时间,也不需要深入理解并发和分布式系统的内部细节。Spark 是在 2009 年由 UC Berkeley 的 AMPLab 团队实现的,2010 年他们决定将其开源。之后,Spark 于 2013 年成为 Apache 项目,从那时起,Spark 一直被认为是最著名和最常用的 Apache 开源软件。Apache Spark 因其以下特性而声名显赫:

  • 快速计算:由于其独特的内存计算特性,Spark 能够帮助你比 Hadoop 更快地运行应用程序。

  • 支持多种编程语言:Apache Spark 为不同的编程语言提供了封装和内置 API,如 Scala、Java、Python,甚至 R。

  • 更多分析功能:如前所述,Spark 支持 MapReduce 操作,同时也支持更高级的分析功能,如机器学习MLlib)、数据流处理和图形处理算法。

如前所述,Spark 是构建在 Hadoop 软件之上的,你可以以不同的方式部署 Spark:

  • 独立集群:这意味着 Spark 将运行在Hadoop 分布式文件系统HDFS)之上,并将空间实际分配给 HDFS。Spark 和 MapReduce 将并行运行,以服务所有 Spark 作业。

  • Hadoop YARN 集群:这意味着 Spark 可以直接在 YARN 上运行,无需任何根权限或预先安装。

  • Mesos 集群:当驱动程序创建一个 Spark 作业并开始为调度分配相关任务时,Mesos 会决定哪些计算节点将处理哪些任务。我们假设你已经在你的机器上配置并安装了 Mesos。

  • 按需付费集群部署:你可以在 AWS EC2 上以真实集群模式部署 Spark 作业。为了让应用在 Spark 集群模式下运行并提高可扩展性,你可以考虑将亚马逊弹性计算云EC2)服务作为基础设施即服务IaaS)或平台即服务PaaS)。

请参考第十七章,前往 ClusterLand - 在集群上部署 Spark 和第十八章,在集群上测试和调试 Spark,了解如何使用 Scala 和 Spark 在真实集群上部署数据分析应用程序。

Scala 和 Spark 编程模型

Spark 编程从一个数据集或几个数据集开始,通常位于某种形式的分布式持久化存储中,例如 HDFS。Spark 提供的典型 RDD 编程模型可以描述如下:

  • 从环境变量中,Spark 上下文(Spark Shell 为你提供了 Spark 上下文,或者你可以自己创建,稍后将在本章中讨论)创建一个初始数据引用的 RDD 对象。

  • 转换初始 RDD,创建更多的 RDD 对象,遵循函数式编程风格(稍后会讨论)。

  • 将代码、算法或应用程序从驱动程序发送到集群管理器节点,然后集群管理器会将副本提供给每个计算节点。

  • 计算节点持有其分区中 RDD 的引用(同样,驱动程序也持有数据引用)。然而,计算节点也可以由集群管理器提供输入数据集。

  • 经过一次转换(无论是狭义转换还是宽义转换)后,生成的结果是一个全新的 RDD,因为原始的 RDD 不会被修改。

  • 最终,RDD 对象或更多(具体来说,是数据引用)通过一个动作进行物化,将 RDD 转储到存储中。

  • 驱动程序可以请求计算节点提供一部分结果,用于程序的分析或可视化。

等等!到目前为止,我们已经顺利进行。我们假设你会将应用程序代码发送到集群中的计算节点。但是,你仍然需要将输入数据集上传或发送到集群中,以便在计算节点之间进行分发。即使在批量上传时,你也需要通过网络传输数据。我们还认为,应用程序代码和结果的大小是可以忽略不计的或微不足道的。另一个障碍是,如果你希望 Spark 进行大规模数据处理,可能需要先将数据对象从多个分区合并。这意味着我们需要在工作节点/计算节点之间进行数据洗牌,通常通过 partition()intersection()join() 等转换操作来实现。

Scala 和 Spark 生态系统

为了提供更多增强功能和大数据处理能力,Spark 可以配置并运行在现有的基于 Hadoop 的集群上。另一方面,Spark 中的核心 API 是用 Java、Scala、Python 和 R 编写的。与 MapReduce 相比,Spark 提供了更通用、更强大的编程模型,并且提供了多个库,这些库是 Spark 生态系统的一部分,能够为通用数据处理与分析、大规模结构化 SQL、图形处理和 机器学习ML)等领域提供附加能力。

Spark 生态系统由以下组件组成,如所示(详细信息请参见 第十六章,Spark 调优):

  • Apache Spark 核心:这是 Spark 平台的底层引擎,所有其他功能都基于它构建。此外,它还提供内存处理功能。

  • Spark SQL:正如之前所提到的,Spark 核心是底层引擎,所有其他组件或功能都是基于它构建的。Spark SQL 是 Spark 组件之一,提供对不同数据结构(结构化和半结构化数据)的支持。

  • Spark Streaming:这个组件负责流式数据分析,并将其转换成可以后续用于分析的小批次数据。

  • MLlib(机器学习库):MLlib 是一个机器学习框架,支持以分布式方式实现许多机器学习算法。

  • GraphX:一个分布式图框架,构建在 Spark 之上,以并行方式表达用户定义的图形组件。

如前所述,大多数函数式编程语言允许用户编写优雅、模块化和可扩展的代码。此外,函数式编程通过编写看起来像数学函数的函数,鼓励安全的编程方式。那么,Spark 是如何使所有的 API 工作成为一个整体的呢?这得益于硬件的进步,以及当然,还有函数式编程的概念。因为简单地为语言增加语法糖以便轻松使用 lambda 表达式并不足以让一种语言具备函数式编程特性,这仅仅是一个开始。

尽管 Spark 中的 RDD 概念运作得很好,但在许多使用案例中,由于其不可变性,情况会变得有些复杂。对于以下计算平均值的经典示例,要使源代码更健壮和可读;当然,为了降低总体成本,人们不希望先计算总和,再计算计数,即使数据已缓存于主内存中。

val data: RDD[People] = ...
data.map(person => (person.name, (person.age, 1)))
.reduceByKey(_ |+| _)
.mapValues { case (total, count) =>
  total.toDouble / count
}.collect()

DataFrames API(这将在后面的章节中详细讨论)生成的代码同样简洁且可读,其中函数式 API 适用于大多数使用场景,最小化了 MapReduce 阶段;有许多 shuffle 操作可能导致显著的性能开销,导致这种情况的主要原因如下:

  • 大型代码库需要静态类型来消除琐碎的错误,比如ae代替age这样的错误

  • 复杂代码需要透明的 API 来清晰地传达设计

  • 通过幕后变异,DataFrames API 可以实现 2 倍的加速,这也可以通过封装状态的 OOP 和使用 mapPartitions 与 combineByKey 来实现

  • 构建功能快速的灵活性和 Scala 特性是必需的

在 Barclays,OOP 和 FP 的结合可以使一些本来非常困难的问题变得更加简单。例如,在 Barclays,最近开发了一个名为 Insights Engine 的应用程序,它可以执行任意数量的接近任意的类似 SQL 查询。该应用程序能够以可扩展的方式执行这些查询,随着 N 的增加而扩展。

现在让我们谈谈纯函数、高阶函数和匿名函数,这三者是 Scala 函数式编程中的三个重要概念。

纯函数与高阶函数

从计算机科学的角度来看,函数可以有多种形式,例如一阶函数、高阶函数或纯函数。从数学角度来看也是如此。使用高阶函数时,以下某一操作可以执行:

  • 接受一个或多个函数作为参数,执行某些操作

  • 返回一个函数作为其结果

除高阶函数外,所有其他函数都是一阶函数。然而,从数学角度来看,高阶函数也被称为运算符泛函数。另一方面,如果一个函数的返回值仅由其输入决定,并且当然没有可观察的副作用,则称为纯函数

在这一部分中,我们将简要讨论为什么以及如何在 Scala 中使用不同的函数式范式。特别是,纯函数和高阶函数将被讨论。在本节末尾,我们还将简要概述如何使用匿名函数,因为在使用 Scala 开发 Spark 应用时,这个概念非常常见。

纯函数

函数式编程中最重要的原则之一是纯函数。那么,什么是纯函数,为什么我们要关心它们?在本节中,我们将探讨函数式编程中的这一重要特性。函数式编程的最佳实践之一是实现程序,使得程序/应用程序的核心由纯函数构成,而所有 I/O 函数或副作用(如网络开销和异常)则位于外部公开层。

那么,纯函数有什么好处呢?纯函数通常比普通函数小(尽管这取决于其他因素,比如编程语言),而且对于人脑来说,它们更容易理解和解释,因为它们看起来像一个数学函数。

然而,你可能会反驳这一点,因为大多数开发人员仍然觉得命令式编程更容易理解!纯函数更容易实现和测试。让我们通过一个例子来演示这一点。假设我们有以下两个独立的函数:

def pureFunc(cityName: String) = s"I live in $cityName"
def notpureFunc(cityName: String) = println(s"I live in $cityName")

所以在前面的两个例子中,如果你想测试pureFunc纯函数,我们只需断言从纯函数返回的值与我们基于输入预期的值相符,如:

assert(pureFunc("Dublin") == "I live in Dublin")

但是,另一方面,如果我们想测试我们的notpureFunc非纯函数,那么我们需要重定向标准输出并对其应用断言。下一个实用技巧是,函数式编程使程序员更加高效,因为,如前所述,纯函数更小,更容易编写,并且你可以轻松地将它们组合在一起。除此之外,代码重复最小,你可以轻松地重用你的代码。现在,让我们通过一个更好的例子来演示这一优势。考虑这两个函数:

scala> def pureMul(x: Int, y: Int) = x * y
pureMul: (x: Int, y: Int)Int 
scala> def notpureMul(x: Int, y: Int) = println(x * y)
notpureMul: (x: Int, y: Int)Unit

然而,可能会有可变性带来的副作用;使用纯函数(即没有可变性)有助于我们推理和测试代码:

def pureIncrease(x: Int) = x + 1

这个方法具有优势,非常容易解释和使用。然而,让我们来看另一个例子:

varinc = 0
def impureIncrease() = {
  inc += 1
  inc
}

现在,考虑一下这可能会有多混乱:在多线程环境中,输出会是什么?如你所见,我们可以轻松使用我们的纯函数pureMul来乘以任何数字序列,这与我们的notpureMul非纯函数不同。让我们通过以下例子来演示这一点:

scala> Seq.range(1,10).reduce(pureMul)
res0: Int = 362880

上述示例的完整代码如下(方法已使用一些实际值进行调用):

package com.chapter3.ScalaFP

object PureAndNonPureFunction {
  def pureFunc(cityName: String) = s"I live in $cityName"
  def notpureFunc(cityName: String) = println(s"I live in $cityName")
  def pureMul(x: Int, y: Int) = x * y
  def notpureMul(x: Int, y: Int) = println(x * y)  

  def main(args: Array[String]) {
    //Now call all the methods with some real values
    pureFunc("Galway") //Does not print anything
    notpureFunc("Dublin") //Prints I live in Dublin
    pureMul(10, 25) //Again does not print anything
    notpureMul(10, 25) // Prints the multiplicaiton -i.e. 250   

    //Now call pureMul method in a different way
    val data = Seq.range(1,10).reduce(pureMul)
    println(s"My sequence is: " + data)
  }
}

上述代码的输出如下:

I live in Dublin 250 
My sequence is: 362880

如前所述,你可以将纯函数视为函数式编程中最重要的特性之一,并作为最佳实践;你需要用纯函数构建应用程序的核心。

函数与方法:

在编程领域,函数是通过名称调用的一段代码。数据(作为参数或作为参数)可以传递给函数进行操作,并且可以返回数据(可选)。所有传递给函数的数据都是显式传递的。另一方面,方法也是通过名称调用的一段代码。然而,方法总是与一个对象相关联。听起来相似吗?嗯!在大多数情况下,方法与函数是相同的,只有两个关键的区别:

1. 方法隐式地接收它被调用的对象。

2. 方法能够对包含在类中的数据进行操作。

在前一章中已经说明了,对象是类的实例——类是定义,对象是该数据的实例。

现在是学习高阶函数的时候了。不过,在此之前,我们应该先了解函数式 Scala 中的另一个重要概念——匿名函数。通过这个概念,我们还将学习如何在函数式 Scala 中使用 lambda 表达式。

匿名函数

有时候,在你的代码中,你不想在使用之前定义一个函数,可能是因为你只会在某一个地方使用它。在函数式编程中,有一种非常适合这种情况的函数类型,叫做匿名函数。让我们通过之前的转账示例来演示匿名函数的使用:

def TransferMoney(money: Double, bankFee: Double => Double): Double = {
  money + bankFee(money)
}

现在,让我们使用一些实际值来调用 TransferMoney() 方法,如下所示:

 TransferMoney(100, (amount: Double) => amount * 0.05)

Lambda 表达式:

如前所述,Scala 支持一等函数,这意味着函数也可以通过函数字面量语法表达;函数可以通过对象来表示,被称为函数值。尝试以下表达式,它为整数创建了一个后继函数:

scala> var apply = (x:Int) => x+1

apply: Int => Int = <function1>

现在,apply 变量已经是一个可以像往常一样使用的函数,如下所示:

scala> var x = apply(7)

x: Int = 8

我们在这里所做的就是简单地使用了函数的核心部分:参数列表,接着是函数箭头以及函数体。这并不是黑魔法,而是一个完整的函数,只不过没有给定名称——也就是匿名函数。如果你以这种方式定义一个函数,那么之后就无法引用该函数,因此你无法在之后调用它,因为没有名称,它是匿名的。同时,我们还看到了所谓的lambda 表达式!它就是函数的纯粹匿名定义。

上述代码的输出如下:

105.0

所以,在之前的示例中,我们没有声明一个单独的 callback 函数,而是直接传递了一个匿名函数,它完成了与 bankFee 函数相同的工作。你也可以省略匿名函数中的类型,它将根据传递的参数直接推断出来,像这样:

TransferMoney(100, amount => amount * 0.05)

上述代码的输出如下:

105.0

让我们在 Scala shell 中展示前面的例子,如下截图所示:

图 6: 在 Scala 中使用匿名函数

一些支持函数式编程的编程语言使用“lambda 函数”这个名称来代替匿名函数。

高阶函数

在 Scala 的函数式编程中,你可以将函数作为参数传递,甚至可以将一个函数作为结果从另一个函数返回;这就是所谓的高阶函数。

让我们通过一个例子来演示这个特性。考虑以下函数testHOF,它接受另一个函数func,然后将该函数应用于它的第二个参数值:

object Test {
  def main(args: Array[String]) {
    println( testHOF( paramFunc, 10) )
  }
  def testHOF(func: Int => String, value: Int) = func(value)
  def paramFuncA = "[" + x.toString() + "]"
}

在演示了 Scala 函数式编程的基础后,现在我们准备进入更复杂的函数式编程案例。如前所述,我们可以将高阶函数定义为接受其他函数作为参数并返回它们的结果。如果你来自面向对象编程的背景,你会发现这是一种非常不同的方法,但随着我们继续深入,它会变得更容易理解。

让我们从定义一个简单的函数开始:

def quarterMaker(value: Int): Double = value.toDouble/4

前面的函数是一个非常简单的函数。它接受一个Int值,并返回该值的四分之一,类型为Double。让我们定义另一个简单函数:

def addTwo(value: Int): Int = value + 2

第二个函数addTwo比第一个函数更简单。它接受一个Int值,然后加 2。正如你所看到的,这两个函数有一些相同之处。它们都接受Int并返回另一个处理过的值,我们可以称之为AnyVal。现在,让我们定义一个接受另一个函数作为参数的高阶函数:

def applyFuncOnRange(begin: Int, end: Int, func: Int => AnyVal): Unit = {
  for (i <- begin to end)
    println(func(i))
}

正如你所看到的,前面的函数applyFuncOnRange接受两个Int值,作为序列的开始和结束,并接受一个具有Int => AnyVal签名的函数,就像之前定义的简单函数(quarterMakderaddTwo)。现在,让我们通过将两个简单函数中的一个作为第三个参数传递给它,来展示我们之前的高阶函数(如果你想传递自己的函数,确保它具有相同的签名Int => AnyVal)。

Scala 的范围 for 循环语法: 使用 Scala 范围的 for 循环的最简单语法是:

for( var x <- range ){

statement(s)

}

这里,range可以是一个数字范围,表示为ij,有时也像i直到j。左箭头运算符被称为生成器,因为它从范围中生成单个值。让我们通过一个具体的例子来展示这一特性:

object UsingRangeWithForLoop {

def main(args: Array[String]):Unit= {

var i = 0;

// 使用范围的 for 循环执行

for( i <- 1 to 10){

println( "i 的值: " + i )

}

}

}

前面代码的输出如下:

i 的值: 1

i 的值: 2

i 的值: 3

i 的值: 4

i 的值: 5

i 的值: 6

i 的值: 7

i 的值:8

i 的值:9

i 的值:10

在开始使用这些函数之前,让我们首先定义它们,如下图所示:

图 2: 在 Scala 中定义高阶函数的示例

现在,让我们开始调用我们的高阶函数 applyFuncOnRange 并将 quarterMaker 函数作为第三个参数传递:

图 3: 调用高阶函数

我们甚至可以应用另一个函数 addTwo,因为它具有与上图所示相同的签名:

图 4: 调用高阶函数的另一种方式

在进一步讲解其他示例之前,我们先定义一下回调函数。回调函数是可以作为参数传递给其他函数的函数。其他函数则是普通函数。我们将通过更多的示例来演示如何使用不同的回调函数。考虑以下高阶函数,它负责从你的账户转账指定金额:

def TransferMoney(money: Double, bankFee: Double => Double): Double = {
  money + bankFee(money)
}
def bankFee(amount: Double) = amount * 0.05

在对 100 调用 TransferMoney 函数之后:

TransferMoney(100, bankFee)

上述代码的输出如下所示:

105.0

从函数式编程的角度来看,这段代码尚未准备好集成到银行系统中,因为你需要对金额参数进行不同的验证,例如它必须是正数并且大于银行指定的特定金额。然而,在这里我们仅仅是演示高阶函数和回调函数的使用。

所以,这个示例的工作原理如下:你想将一定金额的资金转账到另一个银行账户或钱款代理人。银行会根据你转账的金额收取特定费用,这时回调函数发挥了作用。它获取要转账的金额,并应用银行费用,以计算出总金额。

TransferMoney 函数接受两个参数:第一个是要转账的金额,第二个是一个回调函数,其签名为 Double => Double,该函数应用于金额参数以确定转账金额的银行费用。

图 5: 调用并为高阶函数提供额外的功能

上述示例的完整源代码如下所示(我们使用一些实际值来调用方法):

package com.chapter3.ScalaFP
object HigherOrderFunction {
  def quarterMaker(value: Int): Double = value.toDouble / 4
  def testHOF(func: Int => String, value: Int) = func(value)
  def paramFuncA = "[" + x.toString() + "]"
  def addTwo(value: Int): Int = value + 2
  def applyFuncOnRange(begin: Int, end: Int, func: Int => AnyVal): Unit = {
    for (i <- begin to end)
      println(func(i))
  }
  def transferMoney(money: Double, bankFee: Double => Double): Double = {
    money + bankFee(money)
  }
  def bankFee(amount: Double) = amount * 0.05
  def main(args: Array[String]) {
    //Now call all the methods with some real values
    println(testHOF(paramFunc, 10)) // Prints [10]
    println(quarterMaker(20)) // Prints 5.0
    println(paramFunc(100)) //Prints [100]
    println(addTwo(90)) // Prints 92
    println(applyFuncOnRange(1, 20, addTwo)) // Prints 3 to 22 and ()
    println(TransferMoney(105.0, bankFee)) //prints 110.25
  }
}

上述代码的输出如下所示:

[10] 
5.0 
[100] 
92 
3 4 5 6 7 8 9 10 11 12 13 14 15 16 1718 19 20 21 22 () 
110.25

通过使用回调函数,你给高阶函数提供了额外的功能;因此,这是一个非常强大的机制,可以使你的程序更加优雅、灵活和高效。

函数作为返回值

如前所述,高阶函数还支持返回一个函数作为结果。我们通过一个示例来演示这一点:

def transferMoney(money: Double) = {
  if (money > 1000)
    (money: Double) => "Dear customer we are going to add the following
                        amount as Fee: "+money * 0.05
  else
    (money: Double) => "Dear customer we are going to add the following
                        amount as Fee: "+money * 0.1
} 
val returnedFunction = TransferMoney(1500)
returnedFunction(1500)

上面的代码片段将输出以下内容:

Dear customer, we are going to add the following amount as Fee: 75.0

让我们运行之前的示例,如下图所示;它展示了如何将函数作为返回值使用:

图 7: 函数作为返回值

前面示例的完整代码如下:

package com.chapter3.ScalaFP
object FunctionAsReturnValue {
  def transferMoney(money: Double) = {
    if (money > 1000)
      (money: Double) => "Dear customer, we are going to add following
                          amount as Fee: " + money * 0.05
    else
      (money: Double) => "Dear customer, we are going to add following
                          amount as Fee: " + money * 0.1
  }  
  def main(args: Array[String]) {
    val returnedFunction = transferMoney(1500.0)
    println(returnedFunction(1500)) //Prints Dear customer, we are 
                         going to add following amount as Fee: 75.0
  }
}

前面代码的输出如下:

Dear customer, we are going to add following amount as Fee: 75.0

在结束我们关于高阶函数的讨论之前,来看看一个实际的例子,也就是使用高阶函数进行柯里化。

使用高阶函数

假设你在餐厅做厨师,某个同事问你一个问题:实现一个高阶函数HOF)来执行柯里化。需要线索吗?假设你有以下两个高阶函数的签名:

def curryX,Y,Z => Z) : X => Y => Z

类似地,按如下方式实现一个执行反柯里化的函数:

def uncurryX,Y,Z: (X,Y) => Z

那么,如何使用高阶函数来执行柯里化操作呢?你可以创建一个特征,它封装了两个高阶函数(即柯里化和反柯里化)的签名,如下所示:

trait Curry {
  def curryA, B, C => C): A => B => C
  def uncurryA, B, C: (A, B) => C
}

现在,你可以按照以下方式实现并扩展这个特征作为一个对象:


object CurryImplement extends Curry {
  def uncurryX, Y, Z: (X, Y) => Z = { (a: X, b: Y) => f(a)(b) }
  def curryX, Y, Z => Z): X => Y => Z = { (a: X) => { (b: Y) => f(a, b) } }
}

这里我先实现了反柯里化,因为它更简单。等号后面的两个大括号是一个匿名函数字面量,用来接受两个参数(即类型为 XYab)。然后,这两个参数可以用于一个返回函数的函数中。接着,它将第二个参数传递给返回的函数。最后,返回第二个函数的值。第二个函数字面量接受一个参数并返回一个新函数,也就是 curry()。最终,当调用时,它返回一个函数,而该函数再返回另一个函数。

现在来看看如何在实际应用中使用前面的对象,该对象扩展了基础特征。这里有一个例子:

object CurryingHigherOrderFunction {
  def main(args: Array[String]): Unit = {
    def add(x: Int, y: Long): Double = x.toDouble + y
    val addSpicy = CurryImplement.curry(add) 
    println(addSpicy(3)(1L)) // prints "4.0"    
    val increment = addSpicy(2) 
    println(increment(1L)) // prints "3.0"    
    val unspicedAdd = CurryImplement.uncurry(addSpicy) 
    println(unspicedAdd(1, 6L)) // prints "7.0"
  }
}

在前面的对象中以及主方法内部:

  • addSpicy 保存了一个函数,该函数接受一个长整型并将其加 1,然后打印出 4.0。

  • increment 保存了一个函数,该函数接受一个长整型并将其加 2,最后打印出 3.0。

  • unspicedAdd 保存了一个函数,该函数将 1 加到一个长整型上,最后打印出 7.0。

前面代码的输出如下:

4.0
3.0
7.0

在数学和计算机科学中,柯里化是一种技术,它将一个接受多个参数(或一个元组的参数)的函数的求值转换为一系列函数的求值,每个函数只接受一个参数。柯里化与部分应用相关,但不同于部分应用:

柯里化: 柯里化在实践和理论环境中都非常有用。在函数式编程语言以及许多其他编程语言中,柯里化提供了一种自动管理函数参数传递和异常的方式。在理论计算机科学中,它提供了一种简化理论模型的方式来研究具有多个参数的函数,这些模型只接受一个参数。

去柯里化: 去柯里化是柯里化的对偶变换,可以看作是一种去功能化的形式。它接收一个返回值为另一个函数 g 的函数 f,并生成一个新的函数 f′,该函数接受 fg 的参数,并返回 f 和随后 g 对这些参数的应用。这个过程可以反复进行。

到目前为止,我们已经学习了如何处理 Scala 中的纯函数、高阶函数和匿名函数。接下来,我们简要概述如何使用 ThrowTryEitherFuture 扩展高阶函数。

函数式 Scala 中的错误处理

到目前为止,我们专注于确保 Scala 函数的主体完成预定任务且不执行其他操作(即不出现错误或异常)。现在,为了有效使用编程并避免生成易出错的代码,你需要了解如何捕获异常并处理语言中的错误。我们将看到如何利用 Scala 的一些特殊功能,如 TryEitherFuture,扩展高阶函数,超出集合的范围。

Scala 中的失败和异常

首先,让我们定义一般情况下的失败含义(来源:tersesystems.com/2012/12/27/error-handling-in-scala/):

  • 意外的内部失败: 操作因未满足的期望而失败,例如空指针引用、违反的断言或简单的错误状态。

  • 预期的内部失败: 操作故意由于内部状态而失败,例如黑名单或断路器。

  • 预期的外部失败: 操作因为被要求处理某些原始输入而失败,如果原始输入无法处理,则会失败。

  • 意外的外部失败: 操作因系统依赖的资源不存在而失败:例如文件句柄丢失、数据库连接失败或网络中断。

不幸的是,除非失败源于某些可管理的异常,否则没有具体方法可以停止失败。另一方面,Scala 使得已检查与未检查变得非常简单:它没有已检查的异常。在 Scala 中,所有异常都是未检查的,甚至是 SQLExceptionIOException 等。因此,接下来我们将看看如何至少处理这些异常。

抛出异常

Scala 方法可能会因意外的工作流而抛出异常。你可以创建一个异常对象,然后使用 throw 关键字抛出它,示例如下:

//code something
throw new IllegalArgumentException("arg 2 was wrong...");
//nothing will be executed from here.

请注意,使用异常处理的主要目标不是生成友好的消息,而是中断 Scala 程序的正常流程。

使用 trycatch 捕获异常

Scala 允许你在一个代码块中使用 try...catch 捕获任何异常,并使用 case 块对其进行模式匹配。使用 try...catch 的基本语法如下:

try
{
  // your scala code should go here
} 
catch
{
  case foo: FooException => handleFooException(foo)
  case bar: BarException => handleBarException(bar)
  case _: Throwable => println("Got some other kind of exception")
}
finally
{
  // your scala code should go here, such as to close a database connection 
}

因此,如果你抛出异常,那么你需要使用try...catch块来优雅地处理它,而不会崩溃并显示内部异常消息:

package com.chapter3.ScalaFP
import java.io.IOException
import java.io.FileReader
import java.io.FileNotFoundException

object TryCatch {
  def main(args: Array[String]) {
    try {
      val f = new FileReader("data/data.txt")
    } catch {
      case ex: FileNotFoundException => println("File not found exception")
      case ex: IOException => println("IO Exception") 
    } 
  }
}

如果在你的项目树的路径/data 下没有名为data.txt的文件,你将遇到如下的FileNotFoundException

前面代码的输出如下:

File not found exception

现在,让我们通过一个简单的示例来展示如何在 Scala 中使用finally子句,以使try...catch块完整。

最后

假设你希望无论是否抛出异常,都执行你的代码,那么你应该使用finally子句。你可以像下面这样将它放在try 块内部。以下是一个示例:

try {
    val f = new FileReader("data/data.txt")
  } catch {
    case ex: FileNotFoundException => println("File not found exception")
  } finally { println("Dude! this code always executes") }
}

现在,这是使用try...catch...finally的完整示例:

package com.chapter3.ScalaFP
import java.io.IOException
import java.io.FileReader
import java.io.FileNotFoundException

object TryCatch {
  def main(args: Array[String]) {
    try {
      val f = new FileReader("data/data.txt")
    } catch {
      case ex: FileNotFoundException => println("File not found 
                                                 exception")
      case ex: IOException => println("IO Exception") 
    } finally {
      println("Finally block always executes!")
    }
  }
}

前面代码的输出如下:

File not found exception 
Finally block always executes!

接下来,我们将讨论 Scala 中的另一个强大特性——Either

创建一个 Either

Either[X, Y]是一个实例,它包含XY中的一个实例,但不能同时包含两个实例。我们将这两个子类型称为 Either 的左侧和右侧。创建一个 Either 很简单。但有时在程序中使用它是非常强大的:

package com.chapter3.ScalaFP
import java.net.URL
import scala.io.Source
object Either {
  def getData(dataURL: URL): Either[String, Source] =
    if (dataURL.getHost.contains("xxx"))
      Left("Requested URL is blocked or prohibited!")
    else
      Right(Source.fromURL(dataURL))      
  def main(args: Array[String]) {
      val either1 = getData(new URL("http://www.xxx.com"))    
      println(either1)      
      val either2 = getData(new URL("http://www.google.com"))    
      println(either2)
  }
}

现在,如果我们传递任何不包含xxx的任意 URL,我们将得到一个被Right子类型封装的Scala.io.Source。如果 URL 包含xxx,那么我们将得到一个被Left子类型封装的String。为了让前述语句更清楚,让我们看看前面代码段的输出:

Left(Requested URL is blocked or prohibited!) Right(non-empty iterator)

接下来,我们将探索 Scala 的另一个有趣特性——Future,它用于以非阻塞的方式执行任务。这也是在任务完成后处理结果的更好方法。

Future

如果你仅仅想以非阻塞的方式运行任务,并且需要在任务完成后处理结果,Scala 为你提供了 Futures。例如,如果你想并行地进行多个 Web 服务调用,并在 Web 服务处理完所有这些调用后与结果一起工作。以下部分将提供一个使用 Future 的示例。

运行一个任务,但进行阻塞

以下示例展示了如何创建一个 Future,然后通过阻塞执行序列来等待其结果。创建 Futures 很简单。你只需要将它传递给你想要的代码。以下示例在未来执行 2+2,然后返回结果:

package com.chapter3.ScalaFP
import scala.concurrent.ExecutionContext.Implicits.global
import scala.concurrent.duration._
import scala.concurrent.{Await, Future}

object RunOneTaskbutBlock {
  def main(args: Array[String]) {
    // Getting the current time in Milliseconds
    implicit val baseTime = System.currentTimeMillis    
    // Future creation
    val testFuture = Future {
      Thread.sleep(300)
      2 + 2
    }    
    // this is the blocking part
    val finalOutput = Await.result(testFuture, 2 second)
    println(finalOutput)
  }
}

Await.result方法等待最多 2 秒,直到Future返回结果;如果它在 2 秒内没有返回结果,它会抛出以下异常,你可能想要处理或捕获该异常:

java.util.concurrent.TimeoutException

是时候总结这一章了。然而,我想借此机会讨论一下我对 Scala 中函数式编程与对象可变性的重要看法。

函数式编程与数据可变性

纯函数式编程是函数式编程中的最佳实践之一,你应该坚持使用它。编写纯函数将使你的编程生活更加轻松,你将能够编写易于维护和扩展的代码。此外,如果你希望并行化代码,编写纯函数将使这一过程更加简单。

如果你是一个函数式编程(FP)的纯粹主义者,使用 Scala 进行函数式编程的一个缺点是 Scala 同时支持面向对象编程(OOP)和函数式编程(FP)(见图 1),因此有可能在同一代码库中混合使用这两种编程风格。在本章中,我们看到了几个例子,展示了编写纯函数是容易的。然而,将它们组合成一个完整的应用程序却很困难。你可能会同意,像单子这样的高级主题使得函数式编程看起来令人畏惧。

我和很多人谈过,他们认为递归并不自然。当你使用不可变对象时,你永远不能用其他方式修改它们。你不会有任何时刻允许这样做。这就是不可变对象的关键所在!有时候,我发现纯函数和数据的输入或输出会混淆。然而,当你需要修改时,你可以创建一个包含你修改过字段的对象副本。因此,从理论上讲,不会混淆。最后,只有使用不可变值和递归可能会导致 CPU 使用和内存的性能问题。

总结

在本章中,我们探讨了一些 Scala 中的函数式编程概念。我们了解了什么是函数式编程以及 Scala 如何支持它,为什么它很重要,以及使用函数式概念的优点。我们还看到了学习函数式编程概念对学习 Spark 范式的重要性。我们讨论了纯函数、匿名函数和高阶函数,并通过适当的例子进行了讲解。接着,在本章后面,我们讨论了如何使用 Scala 的标准库在集合外的高阶函数中处理异常。最后,我们讨论了函数式 Scala 如何影响对象的可变性。

在下一章中,我们将对标准库中最突出特性之一——集合 API 进行深入分析。

第四章:集合 API

"我们最终成为谁,取决于在所有教授讲授完我们之后,我们读了什么书。最伟大的大学是一本本书的集合。"

  • 托马斯·卡莱尔

吸引大多数 Scala 用户的一个特点是其集合 API,非常强大、灵活,并且拥有大量的操作功能。广泛的操作范围使得你处理各种数据时变得更加轻松。我们将介绍 Scala 集合 API,包括它们的不同类型和层级,以适应不同类型的数据并解决各种不同的问题。简而言之,本章将涵盖以下内容:

  • Scala 集合 API

  • 类型和层级

  • 性能特性

  • Java 互操作性

  • 使用 Scala 隐式参数

Scala 集合 API

Scala 集合是一个广泛理解且常用的编程抽象,可以区分可变集合和不可变集合。像可变变量一样,可变集合可以在必要时更改、更新或扩展。然而,像不可变变量一样,不可变集合无法更改。大多数集合类被分别放置在 scala.collectionscala.collection.immutablescala.collection.mutable 包中。

这个极其强大的 Scala 特性为你提供了以下的功能,可以用来操作和处理你的数据:

  • 易于使用:例如,它帮助你消除迭代器和集合更新之间的干扰。因此,20-50 个方法的小词汇量应该足以解决数据分析解决方案中的大多数集合问题。

  • 简洁:你可以使用轻量级语法进行函数式操作,结合多个操作,最终你会感觉自己在使用自定义代数。

  • 安全:帮助你在编码时处理大多数错误。

  • 快速:大多数集合对象经过精心调优和优化,使得你的数据计算能够更快速地进行。

  • 通用:集合使你能够对任何类型的数据执行相同的操作,无论在哪里。

在接下来的章节中,我们将探索 Scala 集合 API 的类型和关联层级。我们将展示如何使用集合 API 中的大多数功能的几个示例。

类型和层级

Scala 集合是一个广泛理解且常用的编程抽象,可以区分可变集合和不可变集合。像可变变量一样,可变集合可以在必要时更改、更新或扩展。像不可变变量一样,不可变集合无法更改。大多数使用这些集合的类分别位于 scala.collectionscala.collection.immutablescala.collection.mutable 包中。

以下层级图(图 1)展示了根据 Scala 官方文档,Scala 集合 API 的层级结构。这些都是高级抽象类或特质,既有可变的实现,也有不可变的实现。

图 1: 包下的集合 scala.collection

Traversable

Traversable 是集合层级结构的根。在 Traversable 中,定义了 Scala 集合 API 提供的广泛操作。Traversable 中只有一个抽象方法,即 foreach 方法。

def foreachU: Unit

该方法对 Traversable 中包含的所有操作至关重要。如果你研究过数据结构,你会熟悉遍历数据结构的元素并在每个元素上执行一个函数。foreach 方法正是做这样的事情,它遍历集合中的元素并对每个元素执行函数 f。正如我们提到的,这是一个抽象方法,它被设计成根据底层集合的不同,提供不同的定义,以确保每个集合的代码高度优化。

Iterable

Iterable 是 Scala 集合 API 层级图中的第二个根。它有一个抽象方法叫做 iterator,必须在所有其他子集合中实现/定义。它还实现了来自根 Traversable 的 foreach 方法。正如我们所提到的,所有的后代子集合将会覆盖此实现,以进行与该子集合相关的特定优化。

Seq、LinearSeq 和 IndexedSeq

序列与常规 Iterable 有一些区别,它有定义的长度和顺序。Seq 有两个子特质,如 LinearSeqIndexedSeq。让我们快速浏览一下它们。

LinearSeq 是线性序列的基特质。线性序列具有相对高效的 head、tail 和 isEmpty 方法。如果这些方法提供了遍历集合的最快方式,则扩展该特质的集合 Coll 还应该扩展 LinearSeqOptimized[A, Coll[A]]LinearSeq 有三个具体方法:

  • isEmpty: 检查列表是否为空

  • head: 该方法返回列表/序列中的第一个元素

  • tail: 返回列表中的所有元素,但不包括第一个元素。每个继承 LinearSeq 的子集合将会有自己对这些方法的实现,以确保良好的性能。两个继承/扩展的集合是流和列表。

更多内容,请参考此链接:www.scala-lang.org/api/current/scala/collection/LinearSeq.html.

最后,IndexedSeq 有两个方法,它是通过这两个方法定义的:

  • Apply: 通过索引查找元素。

  • length:返回序列的长度。通过索引查找元素需要子集合实现的高效性能。这些有索引的序列有VectorArrayBuffer

可变与不可变

在 Scala 中,你会发现可变和不可变集合。一个集合可以有可变实现和不可变实现。这就是为什么在 Java 中,List不能同时是LinkedListArrayList,但ListLinkedList实现和ArrayList实现的原因。下图展示了scala.collection.immutable包中的所有集合:

图 2: 所有在包scala.collection.immutable中的集合

Scala 默认导入不可变集合,如果你需要使用可变集合,则需要自己导入。现在,为了简要了解包scala.collection.mutable中的所有集合,请参考以下图表:

图 3: 所有在包Scala.collection.mutable中的集合

在每一种面向对象编程(OOP)和函数式编程语言中,数组是一个重要的集合包,帮助我们存储数据对象,之后我们可以非常容易地访问它们。在下一小节中,我们将通过一些示例详细讨论数组。

数组

数组是一个可变的集合。在数组中,元素的顺序会被保留,重复的元素也会被保留。由于是可变的,你可以通过访问其索引号来更改数组中任何元素的值。让我们通过几个例子来演示数组的使用。使用以下代码行来声明一个简单的数组:

val numbers: Array[Int] = ArrayInt // A simple array

现在,打印数组的所有元素:

println("The full array is: ")
  for (i <- numbers) {
    print(" " + i)
  }

现在,打印特定元素:例如,第 3 个元素:

println(numbers(2))

让我们把所有元素求和并打印总和:

var total = 0;
for (i <- 0 to (numbers.length - 1)) {
  total = total + numbers(i)
}
println("Sum: = " + total)

查找最小的元素:

var min = numbers(0)
for (i <- 1 to (numbers.length - 1)) {
  if (numbers(i) < min) min = numbers(i)
}
println("Min is: " + min)

查找最大的元素:

var max = numbers(0);
for (i <- 1 to (numbers.length - 1)) {
  if (numbers(i) > max) max = numbers(i)
}
println("Max is: " + max)

创建和定义数组的另一种方式是使用range()方法,示例如下:

//Creating array using range() method
var myArray1 = range(5, 20, 2)
var myArray2 = range(5, 20)

上面的代码行表示我创建了一个元素在 5 到 20 之间且间隔为 2 的数组。如果你没有指定第 3 个参数,Scala 会假定范围间隔为:

//Creating array using range() method without range difference
var myArray1 = range(5, 20, 2)

现在,让我们看看如何访问元素,示例如下:

// Print all the array elements
for (x <- myArray1) {
  print(" " + x)
}
println()
for (x <- myArray2) {
  print(" " + x)
}

使用concat()方法连接两个数组是完全可能的,示例如下:

//Array concatenation
var myArray3 =  concat( myArray1, myArray2)      
// Print all the array elements
for ( x <- myArray3 ) {
  print(" "+ x)
}

请注意,为了使用range()concat()方法,你需要像下面这样导入 Scala 的Array包:

Import Array._

最后,也可以像下面这样定义和使用多维数组:

var myMatrix = ofDimInt

现在,首先使用前面的数组创建一个矩阵,示例如下:

var myMatrix = ofDimInt
// build a matrix
for (i <- 0 to 3) {
  for (j <- 0 to 3) {
    myMatrix(i)(j) = j
  }
}
println()

按如下方式打印之前的矩阵:

// Print two dimensional array
for (i <- 0 to 3) {
  for (j <- 0 to 3) {
    print(" " + myMatrix(i)(j))
  }
  println()
}

之前示例的完整源代码如下所示:

package com.chapter4.CollectionAPI
import Array._                                                                                         object ArrayExample {
  def main(args: Array[String]) {
    val numbers: Array[Int] = ArrayInt
    // A simple array
    // Print all the element of the array
    println("The full array is: ")
    for (i <- numbers) {
      print(" " + i)
    }
    //Print a particular element for example element 3
    println(numbers(2))
    //Summing all the elements
    var total = 0
    for (i <- 0 to (numbers.length - 1)) {
      total = total + numbers(i)
    }
    println("Sum: = " + total)
    // Finding the smallest element
    var min = numbers(0)
    for (i <- 1 to (numbers.length - 1)) {
      if (numbers(i) < min) min = numbers(i)
    }
    println("Min is: " + min)
    // Finding the largest element
    var max = numbers(0)
    for (i <- 1 to (numbers.length - 1)) {
      if (numbers(i) > max) max = numbers(i)
    }
    println("Max is: " + max)
    //Creating array using range() method
    var myArray1 = range(5, 20, 2)
    var myArray2 = range(5, 20)
    // Print all the array elements
    for (x <- myArray1) {
      print(" " + x)
    }
    println()
    for (x <- myArray2) {
      print(" " + x)
    }
    //Array concatenation
    var myArray3 = concat(myArray1, myArray2)
    // Print all the array elements
    for (x <- myArray3) {
      print(" " + x)
    }
    //Multi-dimensional array
    var myMatrix = ofDimInt
    // build a matrix
    for (i <- 0 to 3) {
      for (j <- 0 to 3) {
        myMatrix(i)(j) = j
      }
    }
    println();
    // Print two dimensional array
    for (i <- 0 to 3) {
      for (j <- 0 to 3) {
        print(" " + myMatrix(i)(j))
      }
      println();
    }
  }
}

你将得到以下输出:

The full array is: 1 2 3 4 5 1 2 3 3 4 53 
Sum: = 33 
Min is: 1 
Max is: 5 
5 7 9 11 13 15 17 19 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 5 7 9 11 13 15 17 19 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 
0 1 2 3 
0 1 2 3 
0 1 2 3 
0 1 2 3

在 Scala 中,列表保持顺序,保留重复元素,并且还检查其不可变性。接下来,让我们在下一小节中查看一些使用 Scala 列表的示例。

列表

如前所述,Scala 提供了可变和不可变集合。不可变集合是默认导入的,但如果需要使用可变集合,则需要自行导入。列表是不可变集合,如果你想保持元素之间的顺序并保留重复元素,它可以被使用。让我们通过一个示例来演示列表如何保持顺序并保留重复元素,同时检查它的不可变性:

scala> val numbers = List(1, 2, 3, 4, 5, 1, 2, 3, 4, 5)
numbers: List[Int] = List(1, 2, 3, 4, 5, 1, 2, 3, 4, 5) 
scala> numbers(3) = 10 
<console>:12: error: value update is not a member of List[Int] 
numbers(3) = 10 ^

你可以使用两种不同的构建块来定义列表。Nil表示List的尾部,之后是一个空的List。因此,前面的例子可以重写为:

scala> val numbers = 1 :: 2 :: 3 :: 4 :: 5 :: 1 :: 2 :: 3:: 4:: 5 :: Nil
numbers: List[Int] = List(1, 2, 3, 4, 5, 1, 2, 3,4, 5

让我们通过下面的详细示例来检查列表及其方法:

package com.chapter4.CollectionAPI

object ListExample {
  def main(args: Array[String]) {
    // List of cities
    val cities = "Dublin" :: "London" :: "NY" :: Nil

    // List of Even Numbers
    val nums = 2 :: 4 :: 6 :: 8 :: Nil

    // Empty List.
    val empty = Nil

    // Two dimensional list
    val dim = 1 :: 2 :: 3 :: Nil ::
                   4 :: 5 :: 6 :: Nil ::
                   7 :: 8 :: 9 :: Nil :: Nil
    val temp = Nil

    // Getting the first element in the list
    println( "Head of cities : " + cities.head )

    // Getting all the elements but the last one
    println( "Tail of cities : " + cities.tail )

    //Checking if cities/temp list is empty
    println( "Check if cities is empty : " + cities.isEmpty )
    println( "Check if temp is empty : " + temp.isEmpty )

    val citiesEurope = "Dublin" :: "London" :: "Berlin" :: Nil
    val citiesTurkey = "Istanbul" :: "Ankara" :: Nil

    //Concatenate two or more lists with :::
    var citiesConcatenated = citiesEurope ::: citiesTurkey
    println( "citiesEurope ::: citiesTurkey : "+citiesConcatenated )

    // using the concat method
    citiesConcatenated = List.concat(citiesEurope, citiesTurkey)
    println( "List.concat(citiesEurope, citiesTurkey) : " +
             citiesConcatenated  )

  }
}

你将得到以下输出:

Head of cities : Dublin
Tail of cities : List(London, NY)
Check if cities is empty : false
Check if temp is empty : true
citiesEurope ::: citiesTurkey : List(Dublin, London, Berlin, Istanbul, Ankara)
List.concat(citiesEurope, citiesTurkey) : List(Dublin, London, Berlin, Istanbul, Ankara)

现在,让我们在下一个小节中快速回顾一下如何在 Scala 应用中使用集合。

集合

集合是最广泛使用的集合之一。在集合中,顺序不会被保留,并且集合不允许重复元素。你可以将其视为数学集合的表示法。让我们通过一个例子来演示这一点,看看集合如何不保留顺序并且不允许重复:

scala> val numbers = Set( 1, 2, 3, 4, 5, 1, 2, 3, 4, 5)
numbers: scala.collection.immutable.Set[Int] = Set(5, 1, 2, 3, 4)

以下源代码展示了在 Scala 程序中使用集合的不同方式:

package com.chapter4.CollectionAPI
object SetExample {
  def main(args: Array[String]) {
    // Empty set of integer type
    var sInteger : Set[Int] = Set()
    // Set of even numbers
    var sEven : Set[Int] = Set(2,4,8,10)
    //Or you can use this syntax
    var sEven2 = Set(2,4,8,10)
    val cities = Set("Dublin", "London", "NY")
    val tempNums: Set[Int] = Set()
    //Finding Head, Tail, and checking if the sets are empty
    println( "Head of cities : " + cities.head )
    println( "Tail of cities : " + cities.tail )
    println( "Check if cities is empty : " + cities.isEmpty )
    println( "Check if tempNums is empty : " + tempNums.isEmpty )
    val citiesEurope = Set("Dublin", "London", "NY")
    val citiesTurkey = Set("Istanbul", "Ankara")
    // Sets Concatenation using ++ operator
    var citiesConcatenated = citiesEurope ++ citiesTurkey
    println( "citiesEurope ++ citiesTurkey : " + citiesConcatenated )
    //Also you can use ++ as a method
    citiesConcatenated = citiesEurope.++(citiesTurkey)
    println( "citiesEurope.++(citiesTurkey) : " + citiesConcatenated )
    //Finding minimum and maximum elements in the set
    val evenNumbers = Set(2,4,6,8)
    // Using the min and max methods
    println( "Minimum element in Set(2,4,6,8) : " + evenNumbers.min )
    println( "Maximum element in Set(2,4,6,8) : " + evenNumbers.max )
  }
}

你将得到以下输出:

Head of cities : Dublin
Tail of cities : Set(London, NY)
Check if cities is empty : false
Check if tempNums is empty : true
citiesEurope ++ citiesTurkey : Set(London, Dublin, Ankara, Istanbul, NY)
citiesEurope.++(citiesTurkey) : Set(London, Dublin, Ankara, Istanbul, NY)
Minimum element in Set(2,4,6,8) : 2
Maximum element in Set(2,4,6,8) : 8

根据我在使用 Java 或 Scala 开发 Spark 应用程序时的个人经验,我发现元组的使用非常频繁,尤其是在不使用任何显式类的情况下对元素集合进行分组。在下一个小节中,我们将看到如何在 Scala 中使用元组。

元组

Scala 元组用于将固定数量的项组合在一起。这个组合的最终目标是帮助匿名函数,这样它们就可以作为一个整体传递。与数组或列表的真正区别在于,元组可以包含不同类型的对象,同时保持每个元素的类型信息,而集合则不能,集合使用的类型是公共类型(例如,在前面的例子中,集合的类型是 Set[Any])。

从计算角度来看,Scala 元组也是不可变的。换句话说,元组确实使用类来存储元素(例如,Tuple2Tuple3Tuple22 等)。

以下是一个示例,演示一个元组包含一个整数、一个字符串和控制台:

val tuple_1 = (20, "Hello", Console)

这是以下内容的语法糖(快捷方式):

val t = new Tuple3(20, "Hello", Console)

另一个例子:

scala> val cityPop = ("Dublin", 2)
cityPop: (String, Int) = (Dublin,2)

元组没有命名的访问器让你访问数据,而是需要使用基于位置的访问器,且是从 1 开始计数,而不是从 0 开始。比如:

scala> val cityPop = ("Dublin", 2)
cityPop: (String, Int) = (Dublin,2) 
scala> cityPop._1
res3: String = Dublin 
scala> cityPop._2
res4: Int = 2

此外,元组可以完美地适配模式匹配。例如:

cityPop match {
  case ("Dublin", population) => ...
  case ("NY", population) => ...
}

你甚至可以使用特殊运算符->来编写一个简洁的语法,用于表示两个值的元组。例如:

scala> "Dublin" -> 2
res0: (String, Int) = (Dublin,2)

以下是一个更详细的示例,演示元组的功能:

package com.chapter4.CollectionAPI
object TupleExample {
  def main(args: Array[String]) {
    val evenTuple = (2,4,6,8)
    val sumTupleElements =evenTuple._1 + evenTuple._2 + evenTuple._3 + evenTuple._4
    println( "Sum of Tuple Elements: "  + sumTupleElements )      
    // You can also iterate over the tuple and print it's element using the foreach method
    evenTuple.productIterator.foreach{ evenTuple =>println("Value = " + evenTuple )}
  }
}

你将得到以下输出:

Sum of Tuple Elements: 20 Value = 2 Value = 4 Value = 6 Value = 8

现在,让我们深入了解 Scala 中使用映射(Maps)的世界,映射广泛用于存储基本数据类型。

映射

Map是一个Iterable,由键值对(也称为映射或关联)组成。Map也是最广泛使用的集合之一,因为它可以用于存储基本数据类型。例如:

scala> Map(1 -> 2)
res7: scala.collection.immutable.Map[Int,Int] = Map(1 -> 2)                                                scala> Map("X" -> "Y")
res8: scala.collection.immutable.Map[String,String] = Map(X -> Y)

Scala 的Predef对象提供了一种隐式转换,使你可以将key -> value作为pair (key, value)的替代语法。例如,Map("a" -> 10, "b" -> 15, "c" -> 16)Map(("a", 10), ("b", 15), ("c", 16))完全相同,但可读性更好。

此外,Map可以简单地看作是Tuple2的集合:

Map(2 -> "two", 4 -> "four")

上述行将被理解为:

Map((2, "two"), (4, "four"))

在这个示例中,我们可以说通过使用Map,可以存储一个函数,这就是函数式编程语言中函数的全部意义:它们是第一类公民,可以在任何地方使用。

假设你有一个方法,用于查找数组中的最大元素,如下所示:

var myArray = range(5, 20, 2)
  def getMax(): Int = {
    // Finding the largest element
    var max = myArray(0)
    for (i <- 1 to (myArray.length - 1)) {
      if (myArray(i) > max)
        max = myArray(i)
    }
    max
  }

现在,我们将进行映射,使得可以使用Map方法存储数据:

scala> val myMax = Map("getMax" -> getMax()) 
scala> println("My max is: " + myMax )

让我们再看一下使用Map的另一种方式:

scala> Map( 2 -> "two", 4 -> "four")
res9: scala.collection.immutable.Map[Int,String] = Map(2 -> two, 4 -> four)
scala> Map( 1 -> Map("X"-> "Y"))
res10: scala.collection.immutable.Map[Int,scala.collection.immutable.Map[String,String]] = Map(1 -> Map(X -> Y))

以下是一个详细示例,展示Map的功能:

package com.chapter4.CollectionAPI
import Array._

object MapExample {
  var myArray = range(5, 20, 2)

  def getMax(): Int = {
    // Finding the largest element
    var max = myArray(0)
    for (i <- 1 to (myArray.length - 1)) {
      if (myArray(i) > max)
        max = myArray(i)
    }
    max
  }

  def main(args: Array[String]) {
    val capitals = Map("Ireland" -> "Dublin", "Britain" -> "London", 
    "Germany" -> "Berlin")

    val temp: Map[Int, Int] = Map()
    val myMax = Map("getMax" -> getMax())
    println("My max is: " + myMax )

    println("Keys in capitals : " + capitals.keys)
    println("Values in capitals : " + capitals.values)
    println("Check if capitals is empty : " + capitals.isEmpty)
    println("Check if temp is empty : " + temp.isEmpty)

    val capitals1 = Map("Ireland" -> "Dublin", "Turkey" -> "Ankara",
    "Egypt" -> "Cairo")
    val capitals2 = Map("Germany" -> "Berlin", "Saudi Arabia" ->
    "Riyadh")

    // Map concatenation using ++ operator
    var capitalsConcatenated = capitals1 ++ capitals2
    println("capitals1 ++ capitals2 : " + capitalsConcatenated)

    // use two maps with ++ as method
    capitalsConcatenated = capitals1.++(capitals2)
    println("capitals1.++(capitals2)) : " + capitalsConcatenated)

  }
}

你将得到以下输出:

My max is: Map(getMax -> 19)
Keys in capitals : Set(Ireland, Britain, Germany)
Values in capitals : MapLike(Dublin, London, Berlin)
Check if capitals is empty : false
Check if temp is empty : true
capitals1 ++ capitals2 : Map(Saudi Arabia -> Riyadh, Egypt -> Cairo, Ireland -> Dublin, Turkey -> Ankara, Germany -> Berlin)
capitals1.++(capitals2)) : Map(Saudi Arabia -> Riyadh, Egypt -> Cairo, Ireland -> Dublin, Turkey -> Ankara, Germany -> Berlin)

现在,让我们快速了解在 Scala 中使用 Option;它基本上是一个数据容器,可以存储数据。

Option

Option类型在 Scala 程序中使用频繁,可以将其与 Java 中的 null 值进行比较,null 表示没有值。Scala 的Option [T]是一个容器,用来表示给定类型的零个或一个元素。Option [T]可以是Some [T]None对象,表示缺失的值。例如,Scala 的Mapget方法如果找到了与给定键对应的值,会返回Some(值),如果给定键在Map中没有定义,则返回None

Option的基本特征如下所示:

trait Option[T] {
  def get: A // Returns the option's value.
  def isEmpty: Boolean // Returns true if the option is None, false
  otherwise.
  def productArity: Int // The size of this product. For a product
  A(x_1, ..., x_k), returns k
  def productElement(n: Int): Any // The nth element of this product,
  0-based
  def exists(p: (A) => Boolean): Boolean // Returns true if this option
  is nonempty 
  def filter(p: (A) => Boolean): Option[A] // Returns this Option if it
  is nonempty 
  def filterNot(p: (A) => Boolean): Option[A] // Returns this Option if
  it is nonempty or return None.
  def flatMapB => Option[B]): Option[B] // Returns result of
  applying f to this Option's 
  def foreachU => U): Unit // Apply given procedure f to the
  option's value, if it is nonempty.  
  def getOrElseB >: A: B // Returns the option's value
  if the option is nonempty, 
  def isDefined: Boolean // Returns true if the option is an instance
  of Some, false otherwise.
  def iterator: Iterator[A] // Returns a singleton iterator returning
  Option's value if it is nonempty
  def mapB => B): Option[B] // Returns a Some containing
  result of applying f to this Option's 
  def orElseB >: A: Option[B] // Returns
  this Option if it is nonempty
  def orNull // Returns the option's value if it is nonempty,
                or null if it is empty.  
}

例如,在以下代码中,我们试图映射并显示位于一些国家(如印度孟加拉国日本美国)的一些城市:

object ScalaOptions {
  def main(args: Array[String]) {
    val megacity = Map("Bangladesh" -> "Dhaka", "Japan" -> "Tokyo",
    "India" -> "Kolkata", "USA" -> "New York")
    println("megacity.get( \"Bangladesh\" ) : " + 
    show(megacity.get("Bangladesh")))
    println("megacity.get( \"India\" ) : " + 
    show(megacity.get("India")))
  }
}

现在,为了使前面的代码正常工作,我们需要在某处定义show()方法。在这里,我们可以通过 Scala 的模式匹配来使用Option,如下所示:

def show(x: Option[String]) = x match {
  case Some(s) => s
  case None => "?"
}

将这些组合如下所示,应该会打印出我们期望的准确结果:

package com.chapter4.CollectionAPI
object ScalaOptions {
  def show(x: Option[String]) = x match {
    case Some(s) => s
    case None => "?"
  } 
  def main(args: Array[String]) {
    val megacity = Map("Bangladesh" -> "Dhaka", "Japan" -> "Tokyo",
    "India" -> "Kolkata", "USA" -> "New York")
    println("megacity.get( \"Bangladesh\" ) : " +
    show(megacity.get("Bangladesh")))
    println("megacity.get( \"India\" ) : " +
    show(megacity.get("India")))
  }
}

你将得到以下输出:

megacity.get( "Bangladesh" ) : Dhaka
megacity.get( "India" ) : Kolkata

使用getOrElse()方法,当没有值时,可以访问一个值或默认值。例如:

// Using getOrElse() method: 
val message: Option[String] = Some("Hello, world!")
val x: Option[Int] = Some(20)
val y: Option[Int] = None
println("message.getOrElse(0): " + message.getOrElse(0))
println("x.getOrElse(0): " + x.getOrElse(0))
println("y.getOrElse(10): " + y.getOrElse(10))

你将得到以下输出:

message.getOrElse(0): Hello, world!
x.getOrElse(0): 20
y.getOrElse(10): 10

此外,使用isEmpty()方法,你可以检查该 Option 是否为None。例如:

println("message.isEmpty: " + message.isEmpty)
println("x.isEmpty: " + x.isEmpty)
println("y.isEmpty: " + y.isEmpty)

现在,这是完整的程序:

package com.chapter4.CollectionAPI
object ScalaOptions {
  def show(x: Option[String]) = x match {
    case Some(s) => s
    case None => "?"
  }
  def main(args: Array[String]) {
    val megacity = Map("Bangladesh" -> "Dhaka", "Japan" -> "Tokyo",
    "India" -> "Kolkata", "USA" -> "New York")
    println("megacity.get( \"Bangladesh\" ) : " +
    show(megacity.get("Bangladesh")))
    println("megacity.get( \"India\" ) : " +
    show(megacity.get("India")))

    // Using getOrElse() method: 
    val message: Option[String] = Some("Hello, world")
    val x: Option[Int] = Some(20)
    val y: Option[Int] = None

    println("message.getOrElse(0): " + message.getOrElse(0))
    println("x.getOrElse(0): " + x.getOrElse(0))
    println("y.getOrElse(10): " + y.getOrElse(10))

    // Using isEmpty()
    println("message.isEmpty: " + message.isEmpty)
    println("x.isEmpty: " + x.isEmpty)
    println("y.isEmpty: " + y.isEmpty)
  }
}

你将得到以下输出:

megacity.get( "Bangladesh" ) : Dhaka
megacity.get( "India" ) : Kolkata
message.getOrElse(0): Hello, world
x.getOrElse(0): 20
y.getOrElse(10): 10
message.isEmpty: false
x.isEmpty: false
y.isEmpty: true

让我们看看其他一些使用Option的示例。例如,Map.get()方法使用Option来告诉用户他尝试访问的元素是否存在。例如:

scala> val numbers = Map("two" -> 2, "four" -> 4)
numbers: scala.collection.immutable.Map[String,Int] = Map(two -> 2, four -> 4)
scala> numbers.get("four")
res12: Option[Int] = Some(4)
scala> numbers.get("five")
res13: Option[Int] = None

现在,我们将看到如何使用exists,它用于检查一个谓词是否适用于集合中元素的子集。

存在

Exists 检查一个谓词是否对可遍历集合中的至少一个元素成立。例如:

def exists(p: ((A, B)) ⇒ Boolean): Boolean  

使用 fat 箭头: => 被称为右箭头fat 箭头火箭,用于按名称传递参数。这意味着表达式将在访问参数时求值。它实际上是一个零参数函数call: x: () => Boolean的语法糖。让我们看看使用这个操作符的示例如下:

package com.chapter4.CollectionAPI

object UsingFatArrow {

def fliesPerSecond(callback: () => Unit) {

while (true) { callback(); Thread sleep 1000 }

}

def main(args: Array[String]): Unit= {

fliesPerSecond(() => println("时间和潮水不等人,但飞如箭一般..."))

}

}

你将得到如下输出:

时间和潮水不等人,但飞如箭一般...

时间和潮水不等人,但飞如箭一般...

时间和潮水不等人,但飞如箭一般...

时间和潮水不等人,但飞如箭一般...

时间和潮水不等人,但飞如箭一般...

时间和潮水不等人,但飞如箭一般...

下面的代码展示了一个详细示例:

package com.chapter4.CollectionAPI

object ExistsExample {
  def main(args: Array[String]) {
    // Given a list of cities and now check if "Dublin" is included in
    the list     
    val cityList = List("Dublin", "NY", "Cairo")
    val ifExisitsinList = cityList exists (x => x == "Dublin")
    println(ifExisitsinList)

    // Given a map of countries and their capitals check if Dublin is
    included in the Map 
    val cityMap = Map("Ireland" -> "Dublin", "UK" -> "London")
    val ifExistsinMap =  cityMap exists (x => x._2 == "Dublin")
    println(ifExistsinMap)
  }
}

你将得到如下输出:

true
true

注意:在 Scala 中使用中缀操作符:

在之前的示例和随后的章节中,我们使用了 Scala 的中缀表示法。假设你想对复数执行一些操作,并且有一个带有 add 方法的 case 类,用于添加两个复数:

case class Complex(i: Double, j: Double) {
   def plus(other: Complex): Complex = Complex(i + other.i, j + other.j)
 }

现在,为了访问这个类的属性,你需要创建一个对象,如下所示:

val obj = Complex(10, 20)

此外,假设你定义了以下两个复数:

val a = Complex(6, 9)
 val b = Complex(3, -6)

现在,要访问 case 类中的plus()方法,你可以这样做:

val z = obj.plus(a)

这应该给你输出:Complex(16.0,29.0)。然而,如果你像这样调用方法,不是更好吗:

val c = a plus b

它真的像魅力一样有效。这里是完整的示例:

package com.chapter4.CollectionAPI
 object UsingInfix {
   case class Complex(i: Double, j: Double) {
     def plus(other: Complex): Complex = Complex(i + other.i, j + other.j)
   }  
   def main(args: Array[String]): Unit = {    
     val obj = Complex(10, 20)
     val a = Complex(6, 9)
     val b = Complex(3, -6)
     val c = a plus b
     val z = obj.plus(a)
     println(c)
     println(z)
   }
 }

中缀操作符的优先级: 由操作符的第一个字符决定。字符按优先级递增的顺序列出,同行的字符具有相同的优先级:

(all letters)
 |
 ^
 &
 = !
 < >
 :
 + -
 * / %
 (all other special characters)

常规警告: 使用中缀表示法调用常规的非符号方法是不推荐的,只有在显著提高可读性的情况下才应使用。中缀表示法的一个充分动机的示例是在ScalaTest中定义匹配器和其他部分的测试。

Scala 集合包中另一个有趣的元素是使用forall。它用于检查一个谓词是否对Traversable集合中的每个元素成立。在接下来的子章节中,我们将看到它的示例。

Forall

Forall 检查一个谓词是否对Traversable集合中的每个元素成立。可以正式地定义如下:

def forall (p: (A) ⇒ Boolean): Boolean  

让我们看看如下示例:

scala> Vector(1, 2, 8, 10) forall (x => x % 2 == 0)
res2: Boolean = false

在编写 Scala 代码进行预处理时,尤其是我们经常需要筛选选定的数据对象。Scala 集合 API 的过滤功能用于此目的。在下一个小节中,我们将看到使用 filter 的示例。

Filter

filter 选择所有满足特定条件的元素。它可以正式定义如下:

def filter(p: (A) ⇒ Boolean): Traversable[A]  

让我们看一个如下的示例:

scala> //Given a list of tuples (cities, Populations)
scala> // Get all cities that has population more than 5 million
scala> List(("Dublin", 2), ("NY", 8), ("London", 8)) filter (x =>x._2 >= 5)
res3: List[(String, Int)] = List((NY,8), (London,8))

Map 用于通过遍历一个函数作用于集合的所有元素来构建一个新的集合或元素集。在下一个小节中,我们将看到使用 Map 的示例。

Map

Map 用于通过遍历一个函数作用于集合的所有元素来构建一个新的集合或元素集。它可以正式定义如下:

def mapB ⇒ B): Map[B]  

让我们看一个如下的示例:

scala> // Given a list of integers
scala> // Get a list with all the elements square.
scala> List(2, 4, 5, -6) map ( x=> x * x)
res4: List[Int] = List(4, 16, 25, 36)

在使用 Scala 集合 API 时,你经常需要选择列表或数组中的第 n^(个) 元素。例如,在下一个小节中,我们将探讨使用 take 的示例。

Take

Take 用于获取集合中的前 n 个元素。使用 take 的正式定义如下:

def take(n: Int): Traversable[A]

让我们看一个如下的示例:

// Given an infinite recursive method creating a stream of odd numbers.
def odd: Stream[Int] = {
  def odd0(x: Int): Stream[Int] =
    if (x%2 != 0) x #:: odd0(x+1)
    else odd0(x+1)
      odd0(1)
}// Get a list of the 5 first odd numbers.
odd take (5) toList

你将得到如下输出:

res5: List[Int] = List(1, 3, 5, 7, 9)

在 Scala 中,如果你想根据特定的分区函数将特定集合划分为另一个 Traversable 集合的映射,你可以使用 groupBy() 方法。在下一个小节中,我们将展示一些使用 groupBy() 的示例。

GroupBy

GroupBy 用于根据特定的分区函数将特定集合划分为其他 Traversable 集合的映射。它可以正式定义如下:

def groupByK) ⇒ K): Map[K, Map[A, B]]  

让我们看一个如下的示例:

scala> // Given a list of numbers
scala> // Group them as positive and negative numbers.
scala> List(1,-2,3,-4) groupBy (x => if (x >= 0) "positive" else "negative")
res6: scala.collection.immutable.Map[String,List[Int]] = Map(negative -> List(-2, -4), positive -> List(1, 3))

在 Scala 中,如果你想选择 Traversable 集合中的所有元素,除了最后一个,你可以使用 init。在下一个小节中,我们将看到相关示例。

Init

init 选择 Traversable 集合中的所有元素,除了最后一个。它可以正式定义如下:

def init: Traversable[A]  

让我们看一个如下的示例:

scala> List(1,2,3,4) init
res7: List[Int] = List(1, 2, 3)

在 Scala 中,如果你想选择除前 n 个元素外的所有元素,你应该使用 drop。在下一个小节中,我们将看到如何使用 drop。

Drop

drop 用于选择除前 n 个元素外的所有元素。它可以正式定义如下:

def drop(n: Int): Traversable[A]  

让我们看一个如下的示例:

// Drop the first three elements
scala> List(1,2,3,4) drop 3
res8: List[Int] = List(4)

在 Scala 中,如果你想获取一组元素直到满足某个条件,你应该使用 takeWhile。在下一个小节中,我们将看到如何使用 takeWhile

TakeWhile

TakeWhile 用于获取一组元素直到满足某个条件。它可以正式定义如下:

def takeWhile(p: (A) ⇒ Boolean): Traversable[A]  

让我们看一个如下的示例:

// Given an infinite recursive method creating a stream of odd numbers.
def odd: Stream[Int] = {
  def odd0(x: Int): Stream[Int] =
    if (x%2 != 0) x #:: odd0(x+1)
    else odd0(x+1)
      odd0(1)
}
// Return a list of all the odd elements until an element isn't less then 9\. 
odd takeWhile (x => x < 9) toList

你将得到如下输出:

res11: List[Int] = List(1, 3, 5, 7)

在 Scala 中,如果你想省略一组元素直到满足某个条件,你应该使用 dropWhile。我们将在下一个小节中看到一些相关示例。

DropWhile

dropWhile 用于省略一组元素直到满足某个条件。它可以正式定义如下:

def dropWhile(p: (A) ⇒ Boolean): Traversable[A]  

让我们看一个如下的示例:

//Drop values till reaching the border between numbers that are greater than 5 and less than 5
scala> List(2,3,4,9,10,11) dropWhile(x => x <5)
res1: List[Int] = List(9, 10, 11)

在 Scala 中,如果你想要使用你的 用户定义函数 (UDF),使其接受嵌套列表中的函数作为参数,并将输出重新组合,flatMap() 是一个完美的选择。我们将在下一节中看到如何使用 flatMap() 的示例。

FlatMap

FlatMap 接受一个函数作为参数。传递给 flatMap() 的函数并不会作用于嵌套列表,而是会生成一个新的集合。它可以正式定义如下:

def flatMapB ⇒ GenTraversableOnce[B]): Traversable[B]  

让我们来看一个如下的例子:

//Applying function on nested lists and then combining output back together
scala> List(List(2,4), List(6,8)) flatMap(x => x.map(x => x * x))
res4: List[Int] = List(4, 16, 36, 64)

我们已经基本完成了 Scala 集合特性用法的介绍。还需要注意的是,像 Fold()Reduce()Aggregate()Collect()Count()Find()Zip() 等方法可以用于从一个集合转换到另一个集合(例如,toVectortoSeqtoSettoArray)。然而,我们将在接下来的章节中看到这些示例。现在是时候查看不同 Scala 集合 API 的一些性能特性了。

性能特性

在 Scala 中,不同的集合有不同的性能特性,而这些特性是你选择某个集合而非其他集合的原因。在本节中,我们将从操作和内存使用的角度评估 Scala 集合对象的性能特性。在本节末尾,我们将提供一些指导,帮助你根据代码和问题类型选择合适的集合对象。

集合对象的性能特性

以下是根据官方文档,Scala 集合的性能特性。

  • 常量:该操作仅需常量时间。

  • eConst:该操作实际上采用常量时间,但这可能取决于一些假设,例如向量的最大长度或哈希键的分布。

  • 线性:该操作随着集合大小按线性增长。

  • 对数:该操作随着集合大小按对数增长。

  • aConst:该操作采用摊销常量时间。有些操作的调用可能会更长,但如果执行多个操作,平均每次操作只需常量时间。

  • 不适用:操作不支持。

序列类型(不可变)的性能特性在下表中呈现。

不可变 CO*头部尾部应用更新前置附加插入
列表常量常量线性线性常量线性不适用
常量常量线性线性常量线性不适用
向量eConsteConsteConsteConsteConsteConst不适用
常量常量线性线性常量线性线性
队列aConstaConst线性线性常量常量不适用
范围常量常量常量不适用不适用不适用不适用
字符串常量线性常量线性线性线性不适用

表 1: 序列类型(不可变)的性能特性 [*CO== 集合对象]

以下表格显示了 表 1表 3 中描述的操作的含义:

头部用于选择现有序列的前几个元素。
尾部用于选择所有元素,除了第一个,并返回一个新的序列。
应用用于索引目的。
更新用于不可变序列的功能性更新。对于可变序列,它是带有副作用的更新(适用于可变序列的更新)。
前置用于将一个元素添加到现有序列的开头。对于不可变序列,会生成一个新的序列。对于可变序列,现有序列会被修改。
附加用于在现有序列的末尾添加一个元素。对于不可变序列,会生成一个新的序列。对于可变序列,现有序列会被修改。
插入用于在现有序列的任意位置插入一个元素。对于可变序列,可以直接执行此操作。

表 2: 表 1 中描述的操作的含义

序列类型(可变)的性能特征显示在 表 3 中,如下所示:

可变 CO*头部尾部应用更新前置附加插入
ArrayBuffer常量线性常量常量线性aConst线性
ListBuffer常量线性线性线性常量常量线性
StringBuilder常量线性常量常量线性aCconst线性
MutableList常量线性线性线性常量常量线性
队列常量线性线性线性常量常量线性
ArraySeq常量线性常量常量不适用不适用不适用
常量线性线性线性常量线性线性
ArrayStack常量线性常量常量aConst线性线性
数组常量线性常量常量不适用不适用不适用

表 3: 序列类型(可变)的性能特征 [*CO== 集合对象]

有关可变集合和其他类型集合的更多信息,请参考此链接 (docs.scala-lang.org/overviews/collections/performance-characteristics.html)。

集合类型和映射类型的性能特征显示在以下表格中:

集合类型查找添加删除最小值
不可变----
HashSet/HashMapeConsteConsteConst线性
TreeSet/TreeMap对数对数对数对数
BitSet常量线性线性eConst*
ListMap线性线性线性线性
集合类型查找添加删除最小值
可变----
HashSet/HashMapeConsteConsteConst线性
WeakHashMapeConsteConsteConst线性
BitSet常量aConst常量eConst*
TreeSet对数对数对数对数

表 4: 集合和映射类型的性能特性 [ 仅在位图紧凑时适用 ]

以下表格显示了 表 4 中描述的每个操作的含义:

操作含义
查找用于测试一个元素是否包含在集合中。其次,也用于选择与特定键相关联的值。
添加用于向集合中添加一个新元素。同时,也用于向映射中添加一个新的键/值对。
删除用于从集合中移除一个元素或从映射中移除一个键。
最小值用于选择集合中的最小元素或映射中的最小键。

表 5: 表 4 中描述的每个操作的含义

基本的性能指标之一是特定集合对象的内存使用情况。在接下来的章节中,我们将提供一些基于内存使用情况测量这些指标的指南。

集合对象的内存使用情况

有时,会遇到几个基准测试问题,例如:对于你的操作,ListsVectors 快,还是 VectorsLists 快?使用未封装的数组存储原始类型时能节省多少内存?当你做一些性能优化操作,比如预分配数组或使用 while 循环代替 foreach 调用时,究竟有多大影响?var l: List 还是 val b: mutable.Buffer?内存使用情况可以通过不同的 Scala 基准代码进行估算,例如,参考 github.com/lihaoyi/scala-bench

表 6 显示了各种不可变集合的估算大小(字节数),包括 0 元素、1 元素、4 元素以及以四的幂次增长的元素数量,一直到 1,048,576 个元素。尽管大多数是确定性的,但这些值可能会根据你的平台有所变化:

大小01416642561,0244,06916,19265,536262,1441,048,576
向量562162644561,5125,44821,19284,312334,4401,353,1925,412,16821,648,072
数组[对象]1640963361,2965,13620,49681,400323,8561,310,7365,242,89620,971,536
列表16561766562,57610,25640,976162,776647,6962,621,45610,485,77641,943,056
流(非强制)16160160160160160160160160160160160
流(强制)16561766562,57610,25640,976162,776647,6962,621,45610,485,77641,943,056
集合1632968803,72014,24859,288234,648895,0003,904,14414,361,00060,858,616
映射16561761,6486,80026,208109,112428,5921,674,5687,055,27226,947,840111,209,368
有序集合401042488243,12812,34449,208195,368777,2723,145,78412,582,96850,331,704
Queue40802006802,60010,28041,000162,800647,7202,621,48010,485,80041,943,080
String404848721685522,0888,18432,424131,112524,3282,097,192

表 6: 各种集合的估计大小(字节)

以下表格显示了 Scala 中数组的估计大小(字节),包括 0 元素、1 元素、4 元素以及四的幂,直到 1,048,576 元素。尽管大多数情况是确定的,但这些值可能会根据平台的不同而变化:

大小01416642561,0244,06916,19265,536262,1441,048,576
Array[Object]1640963361,2965,13620,49681,400323,8561,310,7365,242,89620,971,536
大小01416642561,0244,06916,19265,536262,1441,048,576
Array[Boolean]16242432802721,0404,08816,20865,552262,1601,048,592
Array[Byte]16242432802721,0404,08816,20865,552262,1601,048,592
Array[Short]162424481445282,0648,16032,400131,088524,3042,097,168
Array[Int]162432802721,0404,11216,29664,784262,1601,048,5924,194,320
Array[Long]1624481445282,0648,20832,568129,552524,3042,097,1688,388,624
Boxed Array[Boolean]1640641123041,0724,14416,32864,816262,1921,048,6244,194,352
Boxed Array[Byte]1640963361,2965,1368,20820,39268,880266,2561,052,6884,198,416
Boxed Array[Short]1640963361,2965,13620,49681,400323,8561,310,7365,230,60820,910,096
Boxed Array[Int]1640963361,2965,13620,49681,400323,8561,310,7365,242,89620,971,536
Boxed Array[Long]16481284641,8087,18428,688113,952453,3921,835,0247,340,04829,360,144

表 7:Scala 中数组的估计大小(字节)

然而,本书并未广泛区分这些,因此我们将省略对这些话题的讨论。有关更多指导,请参阅以下信息框:

有关带有计时代码的 Scala 集合的详细基准测试,请参考 GitHub 上的此链接(github.com/lihaoyi/scala-bench/tree/master/bench/src/main/scala/bench)。

正如我们在第一章中提到的,Scala 入门,Scala 有一个非常丰富的集合 API。Java 也是如此,但两者的集合 API 之间有很多差异。在接下来的章节中,我们将看到一些关于 Java 互操作性的示例。

Java 互操作性

如前所述,Scala 具有非常丰富的集合 API。Java 也是如此,但两者之间有很多差异。例如,两者的 API 都有可迭代(iterable)、迭代器(iterators)、映射(maps)、集合(sets)和序列(sequences)。但是,Scala 有优势;它更加关注不可变集合,并为你提供了更多的操作,以便生成另一个集合。有时,你可能需要使用或访问 Java 集合,或者反之亦然。

JavaConversions 已不再是一个合适的选择。JavaConverters 使得 Scala 和 Java 集合之间的转换更加明确,你会更少遇到那些你并未打算使用的隐式转换。

事实上,做到这一点相当简单,因为 Scala 提供了一种隐式的方式在 JavaConversion 对象中转换这两种 API。因此,你可能会发现以下类型的双向转换:

Iterator               <=>     java.util.Iterator
Iterator               <=>     java.util.Enumeration
Iterable               <=>     java.lang.Iterable
Iterable               <=>     java.util.Collection
mutable.Buffer         <=>     java.util.List
mutable.Set            <=>     java.util.Set
mutable.Map            <=>     java.util.Map
mutable.ConcurrentMap  <=>     java.util.concurrent.ConcurrentMap

为了能够使用这些类型的转换,你需要从 JavaConversions 对象中导入它们。例如:

scala> import collection.JavaConversions._
import collection.JavaConversions._

通过此,你可以在 Scala 集合和其对应的 Java 集合之间进行自动转换:

scala> import collection.mutable._
import collection.mutable._
scala> val jAB: java.util.List[Int] = ArrayBuffer(3,5,7)
jAB: java.util.List[Int] = [3, 5, 7]
scala> val sAB: Seq[Int] = jAB
sAB: scala.collection.mutable.Seq[Int] = ArrayBuffer(3, 5, 7)
scala> val jM: java.util.Map[String, Int] = HashMap("Dublin" -> 2, "London" -> 8)
jM: java.util.Map[String,Int] = {Dublin=2, London=8}

你也可以尝试将其他 Scala 集合转换为 Java 集合。例如:

Seq           =>    java.util.List
mutable.Seq   =>    java.utl.List
Set           =>    java.util.Set
Map           =>    java.util.Map 

Java 不提供区分不可变集合和可变集合的功能。List 将是 java.util.List,在尝试修改其元素时会抛出 Exception。以下是一个示例来演示这一点:

scala> val jList: java.util.List[Int] = List(3,5,7)
jList: java.util.List[Int] = [3, 5, 7]
scala> jList.add(9)
java.lang.UnsupportedOperationException
 at java.util.AbstractList.add(AbstractList.java:148)
 at java.util.AbstractList.add(AbstractList.java:108)
 ... 33 elided

在第二章,面向对象的 Scala 中,我们简要讨论了使用隐式。我们将在接下来的章节中提供关于如何使用隐式的详细讨论。

使用 Scala 隐式

我们在前面的章节中已经讨论了隐式(implicits),但这里我们将看到更多的示例。隐式参数与默认参数非常相似,但它们使用不同的机制来找到默认值。

隐式参数是传递给构造函数或方法的,并标记为隐式,这意味着如果你没有为该参数提供值,编译器将在作用域内搜索隐式值。例如:

scala> def func(implicit x:Int) = print(x) 
func: (implicit x: Int)Unit
scala> func
<console>:9: error: could not find implicit value for parameter x: Int
 func
 ^
scala> implicit val defVal = 2
defVal: Int = 2
scala> func(3)
3

隐式对于集合 API 非常有用。例如,集合 API 使用隐式参数来为这些集合中的许多方法提供 CanBuildFrom 对象。这通常发生在用户不关心这些参数的情况下。

一个限制是,每个方法中只能有一个隐式关键字,并且它必须放在参数列表的开头。以下是一些无效的示例:

scala> def func(implicit x:Int, y:Int)(z:Int) = println(y,x)
<console>:1: error: '=' expected but '(' found.
 def func(implicit x:Int, y:Int)(z:Int) = println(y,x)
 ^

隐式参数的数量: 请注意,您可以有多个隐式参数。但是,您不能有多个隐式参数组。

以下是关于多个隐式参数的内容:

scala> def func(implicit x:Int, y:Int)(implicit z:Int, f:Int) = println(x,y)
<console>:1: error: '=' expected but '(' found.
 def func(implicit x:Int, y:Int)(implicit z:Int, f:Int) = println(x,y)
 ^

函数的最终参数列表可以被标记为隐式。这意味着这些值将在调用时从上下文中获取。换句话说,如果作用域中没有确切类型的隐式值,使用隐式值的源代码将无法编译。原因很简单:因为隐式值必须解析为单一的值类型,所以最好使该类型与其目的相匹配,以避免隐式冲突。

此外,您不需要方法来查找隐式转换。例如:

// probably in a library
class Prefixer(val prefix: String)
def addPrefix(s: String)(implicit p: Prefixer) = p.prefix + s
// then probably in your application
implicit val myImplicitPrefixer = new Prefixer("***")
addPrefix("abc")  // returns "***abc"

当您的 Scala 编译器发现上下文所需要的表达式类型错误时,它将寻找一个隐式函数值来进行类型检查。因此,您的常规方法和标记为隐式的方法的区别在于,当发现Double但需要Int时,编译器会为您插入那个隐式方法。例如:

scala> implicit def doubleToInt(d: Double) = d.toInt
val x: Int = 42.0

之前的代码将与以下内容相同:

scala> def doubleToInt(d: Double) = d.toInt
val x: Int = doubleToInt(42.0)

在第二种情况中,我们手动插入了转换。最初,编译器会自动进行此操作。由于左侧的类型注解,转换是必需的。

在处理数据时,我们常常需要将一种类型转换为另一种类型。Scala 的隐式类型转换为我们提供了这个功能。我们将在接下来的部分看到几个示例。

Scala 中的隐式转换

从类型S到类型T的隐式转换是通过具有函数类型S => T的隐式值,或通过可以转换为该类型值的隐式方法定义的。隐式转换在两种情况下应用(来源:docs.scala-lang.org/tutorials/tour/implicit-conversions):

  • 如果一个表达式e的类型是S,而S不符合该表达式预期的类型T

  • e.m的选择中,e的类型是S,如果选择器m不是S的成员。

好吧,我们已经看到了如何在 Scala 中使用中缀操作符。现在,让我们看看一些 Scala 隐式转换的用例。假设我们有以下代码段:

class Complex(val real: Double, val imaginary: Double) {
  def plus(that: Complex) = new Complex(this.real + that.real, this.imaginary + that.imaginary)
  def minus(that: Complex) = new Complex(this.real - that.real, this.imaginary - that.imaginary)
  def unary(): Double = {
    val value = Math.sqrt(real * real + imaginary * imaginary)
    value
  }
  override def toString = real + " + " + imaginary + "i"
}
object UsingImplicitConversion {
  def main(args: Array[String]): Unit = {
    val obj = new Complex(5.0, 6.0)
    val x = new Complex(4.0, 3.0)
    val y = new Complex(8.0, -7.0)

    println(x) // prints 4.0 + 3.0i
    println(x plus y) // prints 12.0 + -4.0i
    println(x minus y) // -4.0 + 10.0i
    println(obj.unary) // prints 7.810249675906654
  }
}

在前面的代码中,我们定义了一些用于执行加法、减法以及复数的单目运算的方法(即包括实数和虚数)。在main()方法中,我们使用实数调用了这些方法。输出结果如下:

4.0 + 3.0i
12.0 + -4.0i
-4.0 + 10.0i
7.810249675906654

但是,如果我们希望支持将一个普通数字添加到一个复数上,我们该如何实现呢?我们当然可以重载我们的plus方法,接受一个Double参数,这样它就可以支持以下表达式。

val sum = myComplexNumber plus 6.5

为此,我们可以使用 Scala 隐式转换。它支持对实数和复数的隐式转换,用于数学运算。所以,我们可以将那个元组作为隐式转换的参数,并将其转换为 Complex,参考以下内容:

implicit def Tuple2Complex(value: Tuple2[Double, Double]) = new Complex(value._1, value._2)

或者,进行如下的双向到复杂转换:

implicit def Double2Complex(value : Double) = new Complex(value,0.0) 

为了利用这种转换,我们需要导入以下内容:

import ComplexImplicits._ // for complex numbers
import scala.language.implicitConversions // in general

现在,我们可以在 Scala REPL/IDE 上执行如下操作:

val z = 4 plus y
println(z) // prints 12.0 + -7.0i
val p = (1.0, 1.0) plus z
println(p) // prints 13.0 + -6.0i 

你将获得以下输出:

12.0 + -7.0i
13.0 + -6.0i

此示例的完整源代码如下所示:

package com.chapter4.CollectionAPI
import ComplexImplicits._
import scala.language.implicitConversions
class Complex(val real: Double, val imaginary: Double) {
  def plus(that: Complex) = new Complex(this.real + that.real, this.imaginary + that.imaginary)
  def plus(n: Double) = new Complex(this.real + n, this.imaginary)
  def minus(that: Complex) = new Complex(this.real - that.real, this.imaginary - that.imaginary)
  def unary(): Double = {
    val value = Math.sqrt(real * real + imaginary * imaginary)
    value
  }
  override def toString = real + " + " + imaginary + "i"
}
object ComplexImplicits {
  implicit def Double2Complex(value: Double) = new Complex(value, 0.0)
  implicit def Tuple2Complex(value: Tuple2[Double, Double]) = new Complex(value._1, value._2)
}
object UsingImplicitConversion {
  def main(args: Array[String]): Unit = {
    val obj = new Complex(5.0, 6.0)
    val x = new Complex(4.0, 3.0)
    val y = new Complex(8.0, -7.0)
    println(x) // prints 4.0 + 3.0i
    println(x plus y) // prints 12.0 + -4.0i
    println(x minus y) // -4.0 + 10.0i
    println(obj.unary) // prints 7.810249675906654
    val z = 4 plus y
    println(z) // prints 12.0 + -7.0i
    val p = (1.0, 1.0) plus z
    println(p) // prints 13.0 + -6.0i
  }
} 

我们现在已经或多或少覆盖了 Scala 集合 API。还有其他功能,但由于页面限制,我们未能覆盖它们。对此感兴趣的读者如果仍想进一步了解,应该参考此页面 www.scala-lang.org/docu/files/collections-api/collections.html

总结

在本章中,我们看到许多使用 Scala 集合 API 的示例。它非常强大、灵活,并且与许多操作结合在一起。这个广泛的操作范围将使你在处理任何类型的数据时更加轻松。我们介绍了 Scala 集合 API,以及它的不同类型和层次结构。我们还演示了 Scala 集合 API 的功能,以及它如何被用来容纳不同类型的数据,并解决广泛的各种问题。总之,你了解了类型和层次结构、性能特性、Java 互操作性以及隐式转换的使用。因此,这基本上就是学习 Scala 的结束。然而,你将通过接下来的章节继续学习更多的高级主题和操作。

在下一章中,我们将探讨数据分析和大数据,了解大数据所带来的挑战,以及如何通过分布式计算和函数式编程推荐的方法来应对这些挑战。你还将学习到 MapReduce、Apache Hadoop,以及最后的 Apache Spark,并了解它们如何采用这种方法和技术。